views:

297

answers:

5

In my test I created a string with 32000 characters. After repeated execution of the test the BCL StringReader consistently executed in 350us while mine ran in 400us. What kind of secrets are they hiding?

Test:

private void SpeedTest()
{
    String r = "";
    for (int i = 0; i < 1000; i++)
    {
     r += Randomization.GenerateString();
    }

    StopWatch s = new StopWatch();
    s.Start();
    using (var sr = new System.IO.StringReader(r))
    {    
     while (sr.Peek() > -1)
     {
      sr.Read();
     }
    }

    s.Stop();
    _Write(s.Elapsed);
    s.Reset();
    s.Start();

    using (var sr = new MagicSynthesis.StringReader(r))
    {    
     while (sr.PeekNext() > Char.MinValue)
     {
      sr.Next();
     }    
    }

    s.Stop();
    _Write(s.Elapsed);
}

Code:

public unsafe class StringReader : IDisposable
{
 private Char* Base;
 private Char* End;
 private Char* Current;
 private const Char Null = '\0';


 /// <summary></summary>
 public StringReader(String s)
 {
  if (s == null)
   throw new ArgumentNullException("s");   

  Base = (Char*)Marshal.StringToHGlobalUni(s).ToPointer();
  End = (Base + s.Length);
  Current = Base;
 }


 /// <summary></summary>
 public Char Next()
 {
  return (Current < End) ? *(Current++) : Null; 
 }

 /// <summary></summary>
 public String Next(Int32 length)
 {
  String s = String.Empty;

  while (Current < End && length > 0)
  {
   length--;
   s += *(Current++);
  }

  return s;
 }

 /// <summary></summary>
 public Char PeekNext()
 {
  return *(Current); 
 }

 /// <summary></summary>
 public String PeekNext(Int32 length)
 {
  String s = String.Empty;
  Char* a = Current;

  while (Current < End && length > 0)
  {
   length--;
   s += *(Current++);
  }

  Current = a;

  return s;
 }


 /// <summary></summary>
 public Char Previous()
 {
  return ((Current > Base) ? *(--Current) : Null);
 }

 /// <summary></summary>
 public Char PeekPrevious()
 {
  return ((Current > Base) ? *(Current - 1) : Null);
 }


 /// <summary></summary>
 public void Dispose()
 {
  Marshal.FreeHGlobal(new IntPtr(Base));   
 }
}
+4  A: 

Maybe Reflector would help you find your answer?

Darrel Miller
IE: Reflector will let you disassemble the code in the BCL so you can actually see what they are doing yourself.
Oplopanax
I looked at the source with Reflector and there is no unsafe code. To be honest this brings more questions than answers... :(
ChaosPandion
+2  A: 

Couldn't tell after simply looking at your code, but here's the code for StringReader.Read():

public override int Read()
{
    if (this._s == null)
    {
        __Error.ReaderClosed();
    }
    if (this._pos == this._length)
    {
        return -1;
    }
    return this._s[this._pos++];
}

They've got two simple value checks and an array access plus increment, versus your value check and pointer increment. Perhaps it would be useful to look at the IL and see how many ops each compiles down to.

womp
+4  A: 

You can always look at the source code

Oplopanax
+1  A: 

Have you tried profiling your StringReader to see if there are any obvious places where you could save time? This is the most reliable way to determine what the bottlenecks in your code are.

Normally I would suggest profiling your solution against the other but I'm not sure about the viability of profiling the BCL. It's GAC'd and strongly signed which makes instrumentation difficult so you would have to rely on sampling.

JaredPar
+4  A: 

I would bet that Marshal.StringToHGlobalUni() and Marshal.FreeHGlobal(new IntPtr(Base)) have a lot to do with the differences. I'm not sure how StringReader manages the string, but I bet it's not copying it to unmanaged memory.

Looking at the StringReader.Read() method in Reflector shows this:

public override int Read()
{
    if (this._s == null)
    {
        __Error.ReaderClosed();
    }
    if (this._pos == this._length)
    {
        return -1;
    }
    return this._s[this._pos++];
}

The contructor is also just:

public StringReader(string s)
{
    if (s == null)
    {
        throw new ArgumentNullException("s");
    }
    this._s = s;
    this._length = (s == null) ? 0 : s.Length;
}

So, it appear that StringReader just maintains the current position and uses regular indexes to return values.

Edit
In response to your comment, your Next() method does a comparison and an unsafe cast, which probably isn't optimized in any way. StringReader.Read() does simple comparison and returns the character as _pos index in the string, which probably has some optimization by the compiler.

scottm
Even removing the constructor and Dispose from the test still leaves a 20us-30us gap that is mind boggling.
ChaosPandion
Removing the conditional statement put mine 30us ahead. Of course now we have some very unsafe code.
ChaosPandion