6
\$\begingroup\$

Usually I don't start using pointers for a few microseconds here and there.

But with this implementation I started getting results of factor 2.

using System; class BenchmarkPointers { public static void Main() { #if ALLOW_UNSAFE Console.WriteLine("This is safe code"); #else Console.WriteLine("This is unsafe code"); #endif int bs = 0x100000; byte[] buffer = new byte[bs]; Random rnd = new Random(DateTime.Now.GetHashCode()); long total = 0; long avg = 0; int max = 100; DateTime start = DateTime.Now; for(int i = 0; i <= max; i++) { start = DateTime.Now; RandomizeBuffer(ref buffer, bs, rnd); TimeSpan ts = DateTime.Now - start; total += (long)ts.TotalMilliseconds; Console.WriteLine("Pass {0} took {1} ms", i, ts.TotalMilliseconds); } avg = total / max; Console.WriteLine("Avarage time for one pass was: {0}ms\nTotal time over {1} passes was {2}ms", avg, max, total); } public static #if ALLOW_UNSAFE unsafe #endif void RandomizeBuffer(ref byte[] buffer, int bufferSize, Random rnd) { #if ALLOW_UNSAFE fixed(byte * pBuffer = buffer) #endif for(int i = 0; i < bufferSize; i += 4) { int k = rnd.Next(int.MinValue, int.MaxValue); #if ALLOW_UNSAFE // One of the rare moments when I like to use pointers (with type casting) *((int*)(pBuffer + i)) = k; #else byte[] bits = BitConverter.GetBytes(k); buffer[i ] = bits[0]; buffer[i + 1] = bits[1]; buffer[i + 2] = bits[2]; buffer[i + 3] = bits[3]; #endif } } } 

Now essentially I need to put the 32bit integers I get from Random.Next into a byte array.

I started implementing the randomizer as

unsafe void RandomizeBuffer(byte **pBuffer, int bufferSize, Random rnd) 

Thats why I got the size in the signature and thats also why I first used the 'pointery' approach. (Since then I removed one deref in the method body)

Then I remembered that C# has reference parameters so I refactored... but noticed my code took longer to execute.

Hence the little benchmark here.

I'm wondering whether there is a managed/safe way to break up lot's of ints into bytes.

Of course since results can vary from circumstance to circumstance here my latest results:

csc Benchmark.cs ... Avarage time for one pass was: 31ms Total time over 100 passes was 3197ms 

and

csc -define:ALLOW_UNSAFE -unsafe benchmark.cs ... Avarage time for one pass was: 19ms Total time over 100 passes was 1931ms 

Also I noticed with the unsafe execution there is quiet a fluctuation in execution times of the passes. i.E.:

 Pass 85 took 15.6097 ms Pass 86 took 15.6251 ms Pass 87 took 31.2502 ms Pass 88 took 15.626 ms Pass 89 took 15.6246 ms 

I can only imagine it has something to do with the CPU Pipeline. But I am no expert in CPU Architecture or Compiler Internals

Any thoughts on this are welcome!

(Yes also those that say: Don't waste your time with this kind of micro management)


Edit:

Using StopWatch and 1000 passes the results are similar, though the fluctuations have been eliminated.

  • safe - Avarage time for one pass was: 24ms Total time over 1000 passes was 24606ms

  • unsafe - Avarage time for one pass was: 16ms Total time over 1000 passes was 16965ms

\$\endgroup\$
1
  • 2
    \$\begingroup\$Never ever use DateTime.Now to benchmark anything. As you're discovering here, it typically has a resolution of about ~15 ms, which isn't very useful for precise timing. That's why you're getting those fluctuations.\$\endgroup\$
    – Kyle
    CommentedJul 14, 2016 at 22:06

1 Answer 1

8
\$\begingroup\$

If the buffer size is not divisible by 4, the remaining bytes are not set. This is a bug and should be corrected.


If you just need random values in the bytes, Random.NextBytes() does what you are trying to do and will likely be faster than both of your current implementation. It's up to you to decide if you care that the values were technically int32's before being written to the buffer.


When writing a benchmark, Stopwatch will provide more precision than DateTime.Now. I also thing 100 is not a large enough number of iterations to get a good average. As you pointed out, there were outliers that took almost twice as long as the majority of values.

It is probably best to take the seed from a command line argument. This will allow you to remove one more variable between the two implementation. While I don't expect Random's execution time to change significantly based on the seed, it is best to keep everything else equal when comparing two things.


Arrays have a Length property, so you shouldn't be dealing with the buffer size independently.


As for if it is worth it to make this optimization, that isn't something we can tell you. Have you profiled the execution of the actual program? Is this where your code if spending the majority of it's time? If it's actually IO bound, ~12ms doesn't mean anything. Where as, if this chunk of code is running millions of times each time your program does a single task, ~12ms is huge and well worth optimizing.

\$\endgroup\$
3
  • \$\begingroup\$Thank you for your insight - Your point with the bufferSize % 4 is good! It might have saved me some pain in the future. - I was going to use StopWatch, but essentially was to lazy to type using System.Windows.Forms; But as you said, an actual profiler would do a better Job at ... 'profiling'. - In the application the seed is actually generated via user input, maybe I'll post the code for that in a seperate thread. Again, thank you for your help!\$\endgroup\$
    – MrPaulch
    CommentedJul 14, 2016 at 19:36
  • \$\begingroup\$Stopwatch is in System.Diagnostics. Maybe you were thinking of Timer, which is in Forms, but does something different.\$\endgroup\$CommentedJul 14, 2016 at 21:01
  • \$\begingroup\$Or maybe I was mixing them up, and setteled on the lowest denimonator. (Wasn't using an IDE for the "benchmark")\$\endgroup\$
    – MrPaulch
    CommentedJul 14, 2016 at 21:12

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.