First, to re-iterate a point made in another answer:
Making a helper class which has internal state static
is a bad idea. You lose a lot of flexibility, re-usability and increase testing pain and gain nothing from it.
The major problem with a static class is: You can't easily mock it for unit testing. If you want to unit test something which uses random numbers it's very helpful to be able to feed it a known sequence of random numbers but with a static class you start adding code which is just used for testing and your tests become brittle if you forget to reset the global state to a known point. Calling new SingleRandom()
is hardly much work - if you consider that a problem then maybe an OO language is the wrong choice.
A static class with a state is effectively a singleton which is an anti-pattern. As such your question should not be "what are convincing reason to not use it" but rather "what are convincing reasons to use it".
My other major point is:
My very first thought when reading this was: Classic case of premature optimization.
- Heave you measured that generating the random numbers is the bottle neck?
- Have you measured that you need a spin lock?
I changed your implementation to use standard .NET locking:
public static class StandardLockSingleRandom
{
private static Random random = new Random();
private static object _lock = new object();
public static int Next()
{
lock (_lock)
{
return random.Next();
}
}
public static int Next(int min, int max)
{
lock (_lock)
{
return random.Next(min, max);
}
}
public static double NextDouble()
{
lock (_lock)
{
return random.NextDouble();
}
}
}
This implementation has 30% less code than yours and the code which is there is considerably less complex. So your spin lock better stack up some nice performance.
Let's write some quick benchmark:
class Program
{
static void Main(string[] args)
{
const int numRandomCalls = 100000;
const int numTestLoops = 100;
for (int p = 1; p <= 16; p <<= 1)
{
Console.WriteLine("=========================================================================================================");
Console.WriteLine("Parallelism = {0}, Count of random numbers per iteration {1}", p, numRandomCalls);
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = p };
RunAndMeasure("Spin Locked", numTestLoops, () => { Parallel.For(0, numRandomCalls, parallelOptions, (idx) => { SingleRandom.Next(); }); });
RunAndMeasure("Standard Locked", numTestLoops, () => { Parallel.For(0, numRandomCalls, parallelOptions, (idx) => { StandardLockSingleRandom.Next(); }); });
}
}
private static void RunAndMeasure(string name, int numLoops, Action act)
{
var stopWatch = new Stopwatch();
stopWatch.Start();
for (int i = 0; i < numLoops; ++i)
{
act();
}
stopWatch.Stop();
var total = stopWatch.Elapsed.TotalMilliseconds;
var perIter = total / numLoops;
Console.WriteLine("{0}: # Test Loops = {1}, total time {2:.000}ms, {3:.000}ms per iteration", name, numLoops, total, perIter);
}
}
Basically:
- Obtain 100,000 random numbers at varying degree of parallelism
- Run each test 100 times and build the average
On my machine which is an i7 with hyperthreading enabled (so going much beyond 8 in parallelism is probably not going to change much) results in this output:
=========================================================================================
Parallelism = 1, Count of random numbers per iteration 100000
Spin Locked: # Test Loops = 100, total time 996.986ms, 9.970ms per iteration
Standard Locked: # Test Loops = 100, total time 482.924ms, 4.829ms per iteration
=========================================================================================
Parallelism = 2, Count of random numbers per iteration 100000
Spin Locked: # Test Loops = 100, total time 1144.200ms, 11.442ms per iteration
Standard Locked: # Test Loops = 100, total time 560.377ms, 5.604ms per iteration
=========================================================================================
Parallelism = 4, Count of random numbers per iteration 100000
Spin Locked: # Test Loops = 100, total time 1253.103ms, 12.531ms per iteration
Standard Locked: # Test Loops = 100, total time 601.836ms, 6.018ms per iteration
=========================================================================================
Parallelism = 8, Count of random numbers per iteration 100000
Spin Locked: # Test Loops = 100, total time 1592.358ms, 15.924ms per iteration
Standard Locked: # Test Loops = 100, total time 802.485ms, 8.025ms per iteration
=========================================================================================
Parallelism = 16, Count of random numbers per iteration 100000
Spin Locked: # Test Loops = 100, total time 1603.059ms, 16.031ms per iteration
Standard Locked: # Test Loops = 100, total time 811.937ms, 8.119ms per iteration
So your spin lock implementation is considerably more complex and it takes twice the time.
What do we learn: Do not optimize things before you have measured them.
Random
and threading issues: msmvps.com/blogs/jon_skeet/archive/2009/11/04/… \$\endgroup\$lock
based code, and it ran forty million times per second on an uncontended lock. WithThreadStatic
it gets to ninety million per second per core. \$\endgroup\$