26

What would be faster?

public String Roll()
{
    Random rnd = new Random();
    int roll = rnd.Next(1, 100000);
    if (Regex.IsMatch(roll.ToString(), @"(.)\1{1,}$"))
    {
        return "doubles";
    }
    return "none";
}

Or

public String Roll()
{
    Random rnd = new Random();
    int roll = rnd.Next(1, 100000);
    if (roll.ToString().EndsWith("11") || roll.ToString().EndsWith("22")  || roll.ToString().EndsWith("33")  || roll.ToString().EndsWith("44")  || roll.ToString().EndsWith("55")  || roll.ToString().EndsWith("66")  || roll.ToString().EndsWith("77")  || roll.ToString().EndsWith("88")  || roll.ToString().EndsWith("99")  || roll.ToString().EndsWith("00"))
    {
        return "doubles";
    }
    return "none";
}

I'm currently using a really long if-statement list full with regexes to check if an int ends with doubles, triples, quads, quints, etc... so I would like to know which one is faster before I change all of it.

Jed Fox
  • 2,979
  • 5
  • 28
  • 38
Dysanix Official
  • 842
  • 1
  • 10
  • 18
  • 20
    Profile it. https://ericlippert.com/2012/12/17/performance-rant/ – Rob May 20 '16 at 05:47
  • 1
    Note that what you are doing does not require any string operations at all.. But that is not related to this no-research-shown question. – Alexei Levenkov May 20 '16 at 05:49
  • One more advice, instead of writing that much long `if` statement, you can store your `EndsWith` values to array, then in loop, check if it ends with that or not. – SᴇM May 20 '16 at 05:49
  • 3
    You don't have to downvote me? I have tons and tons of if-statements. I thought it would be faster to ask someone with experience before I change everything – Dysanix Official May 20 '16 at 05:50
  • 2
    @Alexei Levenkov No-research-shown question? I have been Googling for ages for better ways to do this, but always come out to Regexing and String comparison. If you know something better, share it instead of acting higher than me and leaving me in the unknown. – Dysanix Official May 20 '16 at 05:51
  • 1
    Explicitly saying that you did not do any research on the problem before asking not going to bring you a lot of upvotes. – Alexei Levenkov May 20 '16 at 05:51
  • As shown in answer below both of your solutions are actually quite slow... At least you should have avoided redundant calls to ToString and get last 2 digits only once. – Phil1970 May 27 '16 at 18:30
  • One remark: if you test a regex approach, you need to do two things: 1) declare the regex as a `private static readonly` field with `RegexOptions.Compiled` flag, **and** 2) since you need to search at the end of a string, you must also pass `RegexOptions.RightToLeft` flag when building the regex object. – Wiktor Stribiżew May 30 '16 at 19:39
  • 1
    Ah yes, performance testing and benchmark comparison is always interesting IMO. ;) And I have a particular interest especially in performance for text processing (as you can see in [one of my older posts](http://stackoverflow.com/questions/34392763/tryparse-without-actual-parsing-or-any-other-alternative-for-checking-text-forma) - not so well received as your post though). So yes, I am happy for your too! :) – Ian May 31 '16 at 14:25

7 Answers7

36

In your particular case, Regex is actually faster... but it is likely because you use EndsWith with many OR and redundant ToString(). If you simplify the logic, simple string operation will likely be faster.


Here is the performance summary for text processing - from the fastest to the slowest (10 millions loop [Prefer/Non-Prefer 32-bit] - rank is ordered based on the fastest of the two):

  1. Large Lookup Fast Random UInt (not counted for bounty): 219/273 ms - Mine, improved from Evk's
  2. Large Lookup Optimized Random: 228/273 ms - Ivan Stoev's Alternate Solution
  3. Large Lookup Fast Random: 238/294 ms - Evk's Alternative Solution
  4. Large Lookup Parameterless Random: 248/287 ms - Thomas Ayoub

    There are few notes I want to make on this solution (based on the comments below it):

    1. This solution introduces 0.0039% bias towards small numbers (< 100000) (ref: Eric Lippert's blog post, linked by Lucas Trzesniewski)
    2. Does not generate the same number sequence as others while being tested (ref: Michael Liu's comment) - since it changes the way to use Random (from Random.Next(int) to Random.Next()), which is used for the testing itself.

    While the testing cannot be performed with the exact same number sequence for this method as for the rests (as mentioned by Phil1970), I have two points to make:

    1. Some might be interested to look at the implement of Random.Next() vs Random.Next(int) to understand why this solution will still be faster even if the same sequence of numbers are used.
    2. The use of Random in the real case itself will (most of the time) not assume the number sequence to be the same (or predictable) - It is only for testing our method we want the Random sequence to be identical (for fair unit testing purpose). The expected faster result for this method cannot be fully derived from the testing result alone, but by also looking at the Next() vs Next(int) implementation.
  5. Large Look-up: 320/284 ms - Evk

  6. Fastest Optimized Random Modded: 286/333 ms Ivan Stoev
  7. Lookup Optimized Modded: 315/329 ms - Corak
  8. Optimized Modded: 471/330 ms - Stian Standahl
  9. Optimized Modded + Constant: 472/337 - Gjermund Grøneng
  10. Fastest Optimized Modded: 345/340 ms - Gjermund Grøneng
  11. Modded: 496/370 ms - Corak + possibly Alexei Levenkov
  12. Numbers: 537/408 ms - Alois Kraus
  13. Simple: 1668/1176 ms - Mine
  14. HashSet Contains: 2138/1609 ms - Dandré
  15. List Contains: 3013/2465 ms - Another Mine
  16. Compiled Regex: 8956/7675 ms - Radin Gospodinov
  17. Regex: 15032/16640 ms - OP's Solution 1
  18. EndsWith: 24763/20702 ms - OP's Solution 2

Here are my simple test cases:

Random rnd = new Random(10000);
FastRandom fastRnd = new FastRandom(10000);

//OP's Solution 2
public String RollRegex() {
    int roll = rnd.Next(1, 100000);
    if (Regex.IsMatch(roll.ToString(), @"(.)\1{1,}$")) {
        return "doubles";
    } else {
        return "none";
    }
}

//Radin Gospodinov's Solution
Regex optionRegex = new Regex(@"(.)\1{1,}$", RegexOptions.Compiled);
public String RollOptionRegex() {
    int roll = rnd.Next(1, 100000);
    string rollString = roll.ToString();
    if (optionRegex.IsMatch(roll.ToString())) {
        return "doubles";
    } else {
        return "none";
    }
}

//OP's Solution 1
public String RollEndsWith() {
    int roll = rnd.Next(1, 100000);
    if (roll.ToString().EndsWith("11") || roll.ToString().EndsWith("22") || roll.ToString().EndsWith("33") || roll.ToString().EndsWith("44") || roll.ToString().EndsWith("55") || roll.ToString().EndsWith("66") || roll.ToString().EndsWith("77") || roll.ToString().EndsWith("88") || roll.ToString().EndsWith("99") || roll.ToString().EndsWith("00")) {
        return "doubles";
    } else {
        return "none";
    }
}

//My Solution   
public String RollSimple() {
    int roll = rnd.Next(1, 100000);
    string rollString = roll.ToString();
    return roll > 10 && rollString[rollString.Length - 1] == rollString[rollString.Length - 2] ?
        "doubles" : "none";
}

//My Other Solution
List<string> doubles = new List<string>() { "00", "11", "22", "33", "44", "55", "66", "77", "88", "99" };
public String RollAnotherSimple() {
    int roll = rnd.Next(1, 100000);
    string rollString = roll.ToString();
    return rollString.Length > 1 && doubles.Contains(rollString.Substring(rollString.Length - 2)) ?
        "doubles" : "none";
}

//Dandré's Solution
HashSet<string> doublesHashset = new HashSet<string>() { "00", "11", "22", "33", "44", "55", "66", "77", "88", "99" };
public String RollSimpleHashSet() {
    int roll = rnd.Next(1, 100000);
    string rollString = roll.ToString();
    return rollString.Length > 1 && doublesHashset.Contains(rollString.Substring(rollString.Length - 2)) ?
        "doubles" : "none";
}

//Corak's Solution - hinted by Alexei Levenkov too
public string RollModded() { int roll = rnd.Next(1, 100000); return roll % 100 % 11 == 0 ? "doubles" : "none"; }

//Stian Standahl optimizes modded solution
public string RollOptimizedModded() { return rnd.Next(1, 100000) % 100 % 11 == 0 ? "doubles" : "none"; }

//Gjermund Grøneng's method with constant addition
private const string CONST_DOUBLES = "doubles";
private const string CONST_NONE = "none";
public string RollOptimizedModdedConst() { return rnd.Next(1, 100000) % 100 % 11 == 0 ? CONST_DOUBLES : CONST_NONE; }

//Gjermund Grøneng's method after optimizing the Random (The fastest!)
public string FastestOptimizedModded() { return (rnd.Next(99999) + 1) % 100 % 11 == 0 ? CONST_DOUBLES : CONST_NONE; }

//Corak's Solution, added on Gjermund Grøneng's
private readonly string[] Lookup = { "doubles", "none", "none", "none", "none", "none", "none", "none", "none", "none", "none" };
public string RollLookupOptimizedModded() { return Lookup[(rnd.Next(99999) + 1) % 100 % 11]; }

//Evk's Solution, large Lookup
private string[] LargeLookup;
private void InitLargeLookup() {
    LargeLookup = new string[100000];
    for (int i = 0; i < 100000; i++) {
        LargeLookup[i] = i % 100 % 11 == 0 ? "doubles" : "none";
    }
}
public string RollLargeLookup() { return LargeLookup[rnd.Next(99999) + 1]; }

//Thomas Ayoub's Solution
public string RollLargeLookupParameterlessRandom() { return LargeLookup[rnd.Next() % 100000]; }

//Alois Kraus's Solution
public string RollNumbers() {
    int roll = rnd.Next(1, 100000);
    int lastDigit = roll % 10;
    int secondLastDigit = (roll / 10) % 10;

    if (lastDigit == secondLastDigit) {
        return "doubles";
    } else {
        return "none";
    }
}

//Ivan Stoev's Solution
public string FastestOptimizedRandomModded() {
    return ((int)(rnd.Next() * (99999.0 / int.MaxValue)) + 1) % 100 % 11 == 0 ? CONST_DOUBLES : CONST_NONE;
}

//Ivan Stoev's Alternate Solution
public string RollLargeLookupOptimizedRandom() {
    return LargeLookup[(int)(rnd.Next() * (99999.0 / int.MaxValue))];
}

//Evk's Solution using FastRandom
public string RollLargeLookupFastRandom() {
    return LargeLookup[fastRnd.Next(99999) + 1];
}

//My Own Test, using FastRandom + NextUInt
public string RollLargeLookupFastRandomUInt() {
    return LargeLookup[fastRnd.NextUInt() % 99999 + 1];
}

The additional FastRandom class:

//FastRandom's part used for the testing
public class FastRandom {
    // The +1 ensures NextDouble doesn't generate 1.0
    const double REAL_UNIT_INT = 1.0 / ((double)int.MaxValue + 1.0);
    const double REAL_UNIT_UINT = 1.0 / ((double)uint.MaxValue + 1.0);
    const uint Y = 842502087, Z = 3579807591, W = 273326509;

    uint x, y, z, w;

    #region Constructors

    /// <summary>
    /// Initialises a new instance using time dependent seed.
    /// </summary>
    public FastRandom() {
        // Initialise using the system tick count.
        Reinitialise((int)Environment.TickCount);
    }

    /// <summary>
    /// Initialises a new instance using an int value as seed.
    /// This constructor signature is provided to maintain compatibility with
    /// System.Random
    /// </summary>
    public FastRandom(int seed) {
        Reinitialise(seed);
    }

    #endregion

    #region Public Methods [Reinitialisation]

    /// <summary>
    /// Reinitialises using an int value as a seed.
    /// </summary>
    /// <param name="seed"></param>
    public void Reinitialise(int seed) {
        // The only stipulation stated for the xorshift RNG is that at least one of
        // the seeds x,y,z,w is non-zero. We fulfill that requirement by only allowing
        // resetting of the x seed
        x = (uint)seed;
        y = Y;
        z = Z;
        w = W;
    }

    #endregion

    #region Public Methods [System.Random functionally equivalent methods]

    /// <summary>
    /// Generates a random int over the range 0 to int.MaxValue-1.
    /// MaxValue is not generated in order to remain functionally equivalent to System.Random.Next().
    /// This does slightly eat into some of the performance gain over System.Random, but not much.
    /// For better performance see:
    /// 
    /// Call NextInt() for an int over the range 0 to int.MaxValue.
    /// 
    /// Call NextUInt() and cast the result to an int to generate an int over the full Int32 value range
    /// including negative values. 
    /// </summary>
    /// <returns></returns>
    public int Next() {
        uint t = (x ^ (x << 11));
        x = y; y = z; z = w;
        w = (w ^ (w >> 19)) ^ (t ^ (t >> 8));

        // Handle the special case where the value int.MaxValue is generated. This is outside of 
        // the range of permitted values, so we therefore call Next() to try again.
        uint rtn = w & 0x7FFFFFFF;
        if (rtn == 0x7FFFFFFF)
            return Next();
        return (int)rtn;
    }

    /// <summary>
    /// Generates a random int over the range 0 to upperBound-1, and not including upperBound.
    /// </summary>
    /// <param name="upperBound"></param>
    /// <returns></returns>
    public int Next(int upperBound) {
        if (upperBound < 0)
            throw new ArgumentOutOfRangeException("upperBound", upperBound, "upperBound must be >=0");

        uint t = (x ^ (x << 11));
        x = y; y = z; z = w;

        // The explicit int cast before the first multiplication gives better performance.
        // See comments in NextDouble.
        return (int)((REAL_UNIT_INT * (int)(0x7FFFFFFF & (w = (w ^ (w >> 19)) ^ (t ^ (t >> 8))))) * upperBound);
    }

    /// <summary>
    /// Generates a random int over the range lowerBound to upperBound-1, and not including upperBound.
    /// upperBound must be >= lowerBound. lowerBound may be negative.
    /// </summary>
    /// <param name="lowerBound"></param>
    /// <param name="upperBound"></param>
    /// <returns></returns>
    public int Next(int lowerBound, int upperBound) {
        if (lowerBound > upperBound)
            throw new ArgumentOutOfRangeException("upperBound", upperBound, "upperBound must be >=lowerBound");

        uint t = (x ^ (x << 11));
        x = y; y = z; z = w;

        // The explicit int cast before the first multiplication gives better performance.
        // See comments in NextDouble.
        int range = upperBound - lowerBound;
        if (range < 0) {   // If range is <0 then an overflow has occured and must resort to using long integer arithmetic instead (slower).
            // We also must use all 32 bits of precision, instead of the normal 31, which again is slower.  
            return lowerBound + (int)((REAL_UNIT_UINT * (double)(w = (w ^ (w >> 19)) ^ (t ^ (t >> 8)))) * (double)((long)upperBound - (long)lowerBound));
        }

        // 31 bits of precision will suffice if range<=int.MaxValue. This allows us to cast to an int and gain
        // a little more performance.
        return lowerBound + (int)((REAL_UNIT_INT * (double)(int)(0x7FFFFFFF & (w = (w ^ (w >> 19)) ^ (t ^ (t >> 8))))) * (double)range);
    }

    /// <summary>
    /// Generates a random double. Values returned are from 0.0 up to but not including 1.0.
    /// </summary>
    /// <returns></returns>
    public double NextDouble() {
        uint t = (x ^ (x << 11));
        x = y; y = z; z = w;

        // Here we can gain a 2x speed improvement by generating a value that can be cast to 
        // an int instead of the more easily available uint. If we then explicitly cast to an 
        // int the compiler will then cast the int to a double to perform the multiplication, 
        // this final cast is a lot faster than casting from a uint to a double. The extra cast
        // to an int is very fast (the allocated bits remain the same) and so the overall effect 
        // of the extra cast is a significant performance improvement.
        //
        // Also note that the loss of one bit of precision is equivalent to what occurs within 
        // System.Random.
        return (REAL_UNIT_INT * (int)(0x7FFFFFFF & (w = (w ^ (w >> 19)) ^ (t ^ (t >> 8)))));
    }


    /// <summary>
    /// Fills the provided byte array with random bytes.
    /// This method is functionally equivalent to System.Random.NextBytes(). 
    /// </summary>
    /// <param name="buffer"></param>
    public void NextBytes(byte[] buffer) {
        // Fill up the bulk of the buffer in chunks of 4 bytes at a time.
        uint x = this.x, y = this.y, z = this.z, w = this.w;
        int i = 0;
        uint t;
        for (int bound = buffer.Length - 3; i < bound; ) {
            // Generate 4 bytes. 
            // Increased performance is achieved by generating 4 random bytes per loop.
            // Also note that no mask needs to be applied to zero out the higher order bytes before
            // casting because the cast ignores thos bytes. Thanks to Stefan Troschьtz for pointing this out.
            t = (x ^ (x << 11));
            x = y; y = z; z = w;
            w = (w ^ (w >> 19)) ^ (t ^ (t >> 8));

            buffer[i++] = (byte)w;
            buffer[i++] = (byte)(w >> 8);
            buffer[i++] = (byte)(w >> 16);
            buffer[i++] = (byte)(w >> 24);
        }

        // Fill up any remaining bytes in the buffer.
        if (i < buffer.Length) {
            // Generate 4 bytes.
            t = (x ^ (x << 11));
            x = y; y = z; z = w;
            w = (w ^ (w >> 19)) ^ (t ^ (t >> 8));

            buffer[i++] = (byte)w;
            if (i < buffer.Length) {
                buffer[i++] = (byte)(w >> 8);
                if (i < buffer.Length) {
                    buffer[i++] = (byte)(w >> 16);
                    if (i < buffer.Length) {
                        buffer[i] = (byte)(w >> 24);
                    }
                }
            }
        }
        this.x = x; this.y = y; this.z = z; this.w = w;
    }


    //      /// <summary>
    //      /// A version of NextBytes that uses a pointer to set 4 bytes of the byte buffer in one operation
    //      /// thus providing a nice speedup. The loop is also partially unrolled to allow out-of-order-execution,
    //      /// this results in about a x2 speedup on an AMD Athlon. Thus performance may vary wildly on different CPUs
    //      /// depending on the number of execution units available.
    //      /// 
    //      /// Another significant speedup is obtained by setting the 4 bytes by indexing pDWord (e.g. pDWord[i++]=w)
    //      /// instead of adjusting it dereferencing it (e.g. *pDWord++=w).
    //      /// 
    //      /// Note that this routine requires the unsafe compilation flag to be specified and so is commented out by default.
    //      /// </summary>
    //      /// <param name="buffer"></param>
    //      public unsafe void NextBytesUnsafe(byte[] buffer)
    //      {
    //          if(buffer.Length % 8 != 0)
    //              throw new ArgumentException("Buffer length must be divisible by 8", "buffer");
    //
    //          uint x=this.x, y=this.y, z=this.z, w=this.w;
    //          
    //          fixed(byte* pByte0 = buffer)
    //          {
    //              uint* pDWord = (uint*)pByte0;
    //              for(int i=0, len=buffer.Length>>2; i < len; i+=2) 
    //              {
    //                  uint t=(x^(x<<11));
    //                  x=y; y=z; z=w;
    //                  pDWord[i] = w = (w^(w>>19))^(t^(t>>8));
    //
    //                  t=(x^(x<<11));
    //                  x=y; y=z; z=w;
    //                  pDWord[i+1] = w = (w^(w>>19))^(t^(t>>8));
    //              }
    //          }
    //
    //          this.x=x; this.y=y; this.z=z; this.w=w;
    //      }

    #endregion

    #region Public Methods [Methods not present on System.Random]

    /// <summary>
    /// Generates a uint. Values returned are over the full range of a uint, 
    /// uint.MinValue to uint.MaxValue, inclusive.
    /// 
    /// This is the fastest method for generating a single random number because the underlying
    /// random number generator algorithm generates 32 random bits that can be cast directly to 
    /// a uint.
    /// </summary>
    /// <returns></returns>
    public uint NextUInt() {
        uint t = (x ^ (x << 11));
        x = y; y = z; z = w;
        return (w = (w ^ (w >> 19)) ^ (t ^ (t >> 8)));
    }

    /// <summary>
    /// Generates a random int over the range 0 to int.MaxValue, inclusive. 
    /// This method differs from Next() only in that the range is 0 to int.MaxValue
    /// and not 0 to int.MaxValue-1.
    /// 
    /// The slight difference in range means this method is slightly faster than Next()
    /// but is not functionally equivalent to System.Random.Next().
    /// </summary>
    /// <returns></returns>
    public int NextInt() {
        uint t = (x ^ (x << 11));
        x = y; y = z; z = w;
        return (int)(0x7FFFFFFF & (w = (w ^ (w >> 19)) ^ (t ^ (t >> 8))));
    }


    // Buffer 32 bits in bitBuffer, return 1 at a time, keep track of how many have been returned
    // with bitBufferIdx.
    uint bitBuffer;
    uint bitMask = 1;

    /// <summary>
    /// Generates a single random bit.
    /// This method's performance is improved by generating 32 bits in one operation and storing them
    /// ready for future calls.
    /// </summary>
    /// <returns></returns>
    public bool NextBool() {
        if (bitMask == 1) {
            // Generate 32 more bits.
            uint t = (x ^ (x << 11));
            x = y; y = z; z = w;
            bitBuffer = w = (w ^ (w >> 19)) ^ (t ^ (t >> 8));

            // Reset the bitMask that tells us which bit to read next.
            bitMask = 0x80000000;
            return (bitBuffer & bitMask) == 0;
        }

        return (bitBuffer & (bitMask >>= 1)) == 0;
    }

    #endregion
}

The test scenario:

public delegate string RollDelegate();
private void Test() {
    List<string> rollMethodNames = new List<string>(){
        "Large Lookup Fast Random UInt",
        "Large Lookup Fast Random",
        "Large Lookup Optimized Random",
        "Fastest Optimized Random Modded",
        "Numbers",
        "Large Lookup Parameterless Random",
        "Large Lookup",
        "Lookup Optimized Modded",
        "Fastest Optimized Modded",
        "Optimized Modded Const",
        "Optimized Modded",
        "Modded",
        "Simple",
        "Another simple with HashSet",
        "Another Simple",
        "Option (Compiled) Regex",
        "Regex",
        "EndsWith",
    };
    List<RollDelegate> rollMethods = new List<RollDelegate>{
        RollLargeLookupFastRandomUInt,
        RollLargeLookupFastRandom,
        RollLargeLookupOptimizedRandom,
        FastestOptimizedRandomModded,
        RollNumbers,
        RollLargeLookupParameterlessRandom,
        RollLargeLookup,
        RollLookupOptimizedModded,
        FastestOptimizedModded,
        RollOptimizedModdedConst,
        RollOptimizedModded,
        RollModded,
        RollSimple,
        RollSimpleHashSet,
        RollAnotherSimple,
        RollOptionRegex,
        RollRegex,
        RollEndsWith
    };
    int trial = 10000000;
    InitLargeLookup();
    for (int k = 0; k < rollMethods.Count; ++k) {
        rnd = new Random(10000);
        fastRnd = new FastRandom(10000);
        logBox.GetTimeLapse();
        for (int i = 0; i < trial; ++i)
            rollMethods[k]();
        logBox.WriteTimedLogLine(rollMethodNames[k] + ": " + logBox.GetTimeLapse());
    }
}

The result (Prefer 32-Bit):

[2016-05-30 08:20:54.056 UTC] Large Lookup Fast Random UInt: 219 ms
[2016-05-30 08:20:54.296 UTC] Large Lookup Fast Random: 238 ms
[2016-05-30 08:20:54.524 UTC] Large Lookup Optimized Random: 228 ms
[2016-05-30 08:20:54.810 UTC] Fastest Optimized Random Modded: 286 ms
[2016-05-30 08:20:55.347 UTC] Numbers: 537 ms
[2016-05-30 08:20:55.596 UTC] Large Lookup Parameterless Random: 248 ms
[2016-05-30 08:20:55.916 UTC] Large Lookup: 320 ms
[2016-05-30 08:20:56.231 UTC] Lookup Optimized Modded: 315 ms
[2016-05-30 08:20:56.577 UTC] Fastest Optimized Modded: 345 ms
[2016-05-30 08:20:57.049 UTC] Optimized Modded Const: 472 ms
[2016-05-30 08:20:57.521 UTC] Optimized Modded: 471 ms
[2016-05-30 08:20:58.017 UTC] Modded: 496 ms
[2016-05-30 08:20:59.685 UTC] Simple: 1668 ms
[2016-05-30 08:21:01.824 UTC] Another simple with HashSet: 2138 ms
[2016-05-30 08:21:04.837 UTC] Another Simple: 3013 ms
[2016-05-30 08:21:13.794 UTC] Option (Compiled) Regex: 8956 ms
[2016-05-30 08:21:28.827 UTC] Regex: 15032 ms
[2016-05-30 08:21:53.589 UTC] EndsWith: 24763 ms

The result (Non Prefer 32-Bit):

[2016-05-30 08:16:00.934 UTC] Large Lookup Fast Random UInt: 273 ms
[2016-05-30 08:16:01.230 UTC] Large Lookup Fast Random: 294 ms
[2016-05-30 08:16:01.503 UTC] Large Lookup Optimized Random: 273 ms
[2016-05-30 08:16:01.837 UTC] Fastest Optimized Random Modded: 333 ms
[2016-05-30 08:16:02.245 UTC] Numbers: 408 ms
[2016-05-30 08:16:02.532 UTC] Large Lookup Parameterless Random: 287 ms
[2016-05-30 08:16:02.816 UTC] Large Lookup: 284 ms
[2016-05-30 08:16:03.145 UTC] Lookup Optimized Modded: 329 ms
[2016-05-30 08:16:03.486 UTC] Fastest Optimized Modded: 340 ms
[2016-05-30 08:16:03.824 UTC] Optimized Modded Const: 337 ms
[2016-05-30 08:16:04.154 UTC] Optimized Modded: 330 ms
[2016-05-30 08:16:04.524 UTC] Modded: 370 ms
[2016-05-30 08:16:05.700 UTC] Simple: 1176 ms
[2016-05-30 08:16:07.309 UTC] Another simple with HashSet: 1609 ms
[2016-05-30 08:16:09.774 UTC] Another Simple: 2465 ms
[2016-05-30 08:16:17.450 UTC] Option (Compiled) Regex: 7675 ms
[2016-05-30 08:16:34.090 UTC] Regex: 16640 ms
[2016-05-30 08:16:54.793 UTC] EndsWith: 20702 ms

And the picture:

enter image description here

Community
  • 1
  • 1
Ian
  • 30,182
  • 19
  • 69
  • 107
  • Comments are not for extended discussion; this conversation has been [moved to chat](http://chat.stackoverflow.com/rooms/113432/discussion-on-answer-by-ian-which-one-is-faster-regex-or-endswith). – George Stocker May 31 '16 at 15:16
  • 1
    @Ian Great answer! I wanted to add that Regex performance vs Endswith performance is a bit trickier. I've added some information about how they both work in my answer below. – atlaste Jun 02 '16 at 12:27
13

@StianStandahls friend here. This solution is fastest! It is the same as the previous fastest example in @Ians answer, but the random generator is optimized here.

private const string CONST_DOUBLES = "doubles";
private const string CONST_NONE = "none";
public string FastestOptimizedModded()
{
    return (rnd.Next(99999)+1) % 100 % 11 == 0 ? CONST_DOUBLES : CONST_NONE;
}
  • After an internal competition, this was the fastest answer. :) – Stian Standahl May 20 '16 at 10:55
  • 2
    myyyyyyy............ goodnesssssssssss...........!!! +1 I can't believe that there is still even *faster* solution! Today I cannot test the answer anymore, but tomorrow I will definitely include this answer for the test! And of course, linking your name. Just like others' :) pleased to meet you by the way... :) – Ian May 20 '16 at 11:58
  • based on the source code its clear that this solution should be fastest. https://github.com/dotnet/corefx/blob/master/src/System.Runtime.Extensions/src/System/Random.cs – Stian Standahl May 20 '16 at 12:10
  • 1
    I tested it (in different laptop, but all are tested together), it significantly increases the time. Congrats! ;) – Ian May 20 '16 at 14:46
  • 1
    Nice answer. I will definitely use this in my upcoming module. I am bit vary with this RegEx phenomenon. This will help me a lot. – Mukesh Adhvaryu May 27 '16 at 09:37
5

As for the most performance, I believe @Ian already covered that quite nicely. All credits go to him.

One thing that isn't answered to my satisfaction in the Q/A is why Regex'es outperform EndsWith in this case. I felt the need to explain the difference so people realize what solution will probably work better in which scenario.

Endswith

The EndsWith functionality is basically a 'compare' on part of the string in sequential order. Something like this:

bool EndsWith(string haystack, string needle)
{
    bool equal = haystack.Length >= needle.Length;
    for (int i=0; i<needle.Length && equal; ++i)
    {
        equal = s[i] == needle[needle.Length - haystack.Length + i];
    }
    return equal;
}

The code is pretty straight forward; we simply take the first character, see if it matches, then the next, etc - until we hit the end of the string.

Regex

Regex'es work differently. Consider looking for the needle "foofoo" in a very large haystack. The obvious implementation is to at the first character, check if it's an 'f', move to the next character, etc. until we hit the end of the string. However, we can do much better:

Look closely at the task. If we would first look at character 5 of the string, and notice that it's not an 'o' (the last character), we can immediately skip to character 11 and again check if it's an 'o'. That way, we would get a nice improvement over our original code of a factor 6 in the best case and the same performance in the worst case.

Also note that regexes can become more complex with 'or's, 'and's, etc. Doing forward scans no longer makes a lot of sense if we only need to look at the trailing characters.

This is why Regex'es usually work with NFA's that are compiled to DFA's. There's a great online tool here: http://hackingoff.com/compilers/regular-expression-to-nfa-dfa that shows what this looks like (for simple regex'es).

Internally, you can ask .NET to compile a Regex using Reflection.Emit and when you use a regex, you actually evaluate this optimized, compiled state machine (RegexOptions.Compiled).

What you will probably end up with is something that works like this:

bool Matches(string haystack)
{
    char _1;
    int index = 0;

    // match (.)
state0:
    if (index >= haystack.Length) 
    {
        goto stateFail;
    }
    _1 = haystack[index]; 
    state = 1;
    ++index;
    goto state1;

    // match \1{1}
state1:
    if (index >= haystack.Length) 
    {
        goto stateFail;
    }
    if (_1 == haystack[index])
    {
        ++index;
        goto state2;
    }
    goto stateFail;

    // match \1{2,*}$ -- usually optimized away because it always succeeds
state1:
    if (index >= haystack.Length) 
    {
        goto stateSuccess;
    }
    if (_1 == haystack[index])
    {
        ++index;
        goto state2;
    }
    goto stateSuccess;

stateSuccess:
    return true;

stateFail:
    return false;
}

So what's faster?

Well, that depends. There's overhead in determining the NFA/DFA from the expression, compiling the program and for each call looking up the program and evaluating it. For very simple cases, an EndsWith beats the Regex. In this case it's the 'OR's in the EndsWith that make it slower than the Regex.

On the other hand, a Regex is usually something you use multiple times, which means that you only have to compile it once, and simply look it up for each call.

atlaste
  • 30,418
  • 3
  • 57
  • 87
  • Good explanation! :) This is indeed a valuable info to know how Regex works internally. upvoted. – Ian Jun 02 '16 at 13:19
3

A bit more perfomance could be squeezed out if pregenerate whole lookup table for all possible values. This will avoid two modulo divisions in the fastest method and so will be a bit faster:

private string[] LargeLookup;
private void Init() {
    LargeLookup = new string[100000];
    for (int i = 0; i < 100000; i++) {
        LargeLookup[i] = i%100%11 == 0 ? "doubles" : "none";
    }
}

And the method itself is then just:

public string RollLargeLookup() {
    return LargeLookup[rnd.Next(99999) + 1];
}

While looking somewhat contrieved - such methods are often used. For example fastest known poker hand evaluator pregenerates huge array with hunders of thousands of entries (with very clever tricks) and then just makes several simple lookups on this array to evaluate strength of one poker hand over another in no time.

You can make even faster still, by using alternative random number generators. For example if you replace System.Random with this FastRandom class implementation (based on xorshift algorithm) - it will be twice as fast.

If implement both large lookup table and FastRandom - on my computer it shows 100ms vs 220ms of RollLookupOptimizedModded.

Here is the source code of FastRandom class mentioned in my link above:

public class FastRandom
{
    // The +1 ensures NextDouble doesn't generate 1.0
    const double REAL_UNIT_INT = 1.0 / ((double)int.MaxValue + 1.0);
    const double REAL_UNIT_UINT = 1.0 / ((double)uint.MaxValue + 1.0);
    const uint Y = 842502087, Z = 3579807591, W = 273326509;

    uint x, y, z, w;

    #region Constructors

    /// <summary>
    /// Initialises a new instance using time dependent seed.
    /// </summary>
    public FastRandom()
    {
        // Initialise using the system tick count.
        Reinitialise((int)Environment.TickCount);
    }

    /// <summary>
    /// Initialises a new instance using an int value as seed.
    /// This constructor signature is provided to maintain compatibility with
    /// System.Random
    /// </summary>
    public FastRandom(int seed)
    {
        Reinitialise(seed);
    }

    #endregion

    #region Public Methods [Reinitialisation]

    /// <summary>
    /// Reinitialises using an int value as a seed.
    /// </summary>
    /// <param name="seed"></param>
    public void Reinitialise(int seed)
    {
        // The only stipulation stated for the xorshift RNG is that at least one of
        // the seeds x,y,z,w is non-zero. We fulfill that requirement by only allowing
        // resetting of the x seed
        x = (uint)seed;
        y = Y;
        z = Z;
        w = W;
    }

    #endregion

    #region Public Methods [System.Random functionally equivalent methods]

    /// <summary>
    /// Generates a random int over the range 0 to int.MaxValue-1.
    /// MaxValue is not generated in order to remain functionally equivalent to System.Random.Next().
    /// This does slightly eat into some of the performance gain over System.Random, but not much.
    /// For better performance see:
    /// 
    /// Call NextInt() for an int over the range 0 to int.MaxValue.
    /// 
    /// Call NextUInt() and cast the result to an int to generate an int over the full Int32 value range
    /// including negative values. 
    /// </summary>
    /// <returns></returns>
    public int Next()
    {
        uint t = (x ^ (x << 11));
        x = y; y = z; z = w;
        w = (w ^ (w >> 19)) ^ (t ^ (t >> 8));

        // Handle the special case where the value int.MaxValue is generated. This is outside of 
        // the range of permitted values, so we therefore call Next() to try again.
        uint rtn = w & 0x7FFFFFFF;
        if (rtn == 0x7FFFFFFF)
            return Next();
        return (int)rtn;
    }

    /// <summary>
    /// Generates a random int over the range 0 to upperBound-1, and not including upperBound.
    /// </summary>
    /// <param name="upperBound"></param>
    /// <returns></returns>
    public int Next(int upperBound)
    {
        if (upperBound < 0)
            throw new ArgumentOutOfRangeException("upperBound", upperBound, "upperBound must be >=0");

        uint t = (x ^ (x << 11));
        x = y; y = z; z = w;

        // The explicit int cast before the first multiplication gives better performance.
        // See comments in NextDouble.
        return (int)((REAL_UNIT_INT * (int)(0x7FFFFFFF & (w = (w ^ (w >> 19)) ^ (t ^ (t >> 8))))) * upperBound);
    }

    /// <summary>
    /// Generates a random int over the range lowerBound to upperBound-1, and not including upperBound.
    /// upperBound must be >= lowerBound. lowerBound may be negative.
    /// </summary>
    /// <param name="lowerBound"></param>
    /// <param name="upperBound"></param>
    /// <returns></returns>
    public int Next(int lowerBound, int upperBound)
    {
        if (lowerBound > upperBound)
            throw new ArgumentOutOfRangeException("upperBound", upperBound, "upperBound must be >=lowerBound");

        uint t = (x ^ (x << 11));
        x = y; y = z; z = w;

        // The explicit int cast before the first multiplication gives better performance.
        // See comments in NextDouble.
        int range = upperBound - lowerBound;
        if (range < 0)
        {   // If range is <0 then an overflow has occured and must resort to using long integer arithmetic instead (slower).
            // We also must use all 32 bits of precision, instead of the normal 31, which again is slower.  
            return lowerBound + (int)((REAL_UNIT_UINT * (double)(w = (w ^ (w >> 19)) ^ (t ^ (t >> 8)))) * (double)((long)upperBound - (long)lowerBound));
        }

        // 31 bits of precision will suffice if range<=int.MaxValue. This allows us to cast to an int and gain
        // a little more performance.
        return lowerBound + (int)((REAL_UNIT_INT * (double)(int)(0x7FFFFFFF & (w = (w ^ (w >> 19)) ^ (t ^ (t >> 8))))) * (double)range);
    }

    /// <summary>
    /// Generates a random double. Values returned are from 0.0 up to but not including 1.0.
    /// </summary>
    /// <returns></returns>
    public double NextDouble()
    {
        uint t = (x ^ (x << 11));
        x = y; y = z; z = w;

        // Here we can gain a 2x speed improvement by generating a value that can be cast to 
        // an int instead of the more easily available uint. If we then explicitly cast to an 
        // int the compiler will then cast the int to a double to perform the multiplication, 
        // this final cast is a lot faster than casting from a uint to a double. The extra cast
        // to an int is very fast (the allocated bits remain the same) and so the overall effect 
        // of the extra cast is a significant performance improvement.
        //
        // Also note that the loss of one bit of precision is equivalent to what occurs within 
        // System.Random.
        return (REAL_UNIT_INT * (int)(0x7FFFFFFF & (w = (w ^ (w >> 19)) ^ (t ^ (t >> 8)))));
    }


    /// <summary>
    /// Fills the provided byte array with random bytes.
    /// This method is functionally equivalent to System.Random.NextBytes(). 
    /// </summary>
    /// <param name="buffer"></param>
    public void NextBytes(byte[] buffer)
    {
        // Fill up the bulk of the buffer in chunks of 4 bytes at a time.
        uint x = this.x, y = this.y, z = this.z, w = this.w;
        int i = 0;
        uint t;
        for (int bound = buffer.Length - 3; i < bound;)
        {
            // Generate 4 bytes. 
            // Increased performance is achieved by generating 4 random bytes per loop.
            // Also note that no mask needs to be applied to zero out the higher order bytes before
            // casting because the cast ignores thos bytes. Thanks to Stefan Troschьtz for pointing this out.
            t = (x ^ (x << 11));
            x = y; y = z; z = w;
            w = (w ^ (w >> 19)) ^ (t ^ (t >> 8));

            buffer[i++] = (byte)w;
            buffer[i++] = (byte)(w >> 8);
            buffer[i++] = (byte)(w >> 16);
            buffer[i++] = (byte)(w >> 24);
        }

        // Fill up any remaining bytes in the buffer.
        if (i < buffer.Length)
        {
            // Generate 4 bytes.
            t = (x ^ (x << 11));
            x = y; y = z; z = w;
            w = (w ^ (w >> 19)) ^ (t ^ (t >> 8));

            buffer[i++] = (byte)w;
            if (i < buffer.Length)
            {
                buffer[i++] = (byte)(w >> 8);
                if (i < buffer.Length)
                {
                    buffer[i++] = (byte)(w >> 16);
                    if (i < buffer.Length)
                    {
                        buffer[i] = (byte)(w >> 24);
                    }
                }
            }
        }
        this.x = x; this.y = y; this.z = z; this.w = w;
    }


    //      /// <summary>
    //      /// A version of NextBytes that uses a pointer to set 4 bytes of the byte buffer in one operation
    //      /// thus providing a nice speedup. The loop is also partially unrolled to allow out-of-order-execution,
    //      /// this results in about a x2 speedup on an AMD Athlon. Thus performance may vary wildly on different CPUs
    //      /// depending on the number of execution units available.
    //      /// 
    //      /// Another significant speedup is obtained by setting the 4 bytes by indexing pDWord (e.g. pDWord[i++]=w)
    //      /// instead of adjusting it dereferencing it (e.g. *pDWord++=w).
    //      /// 
    //      /// Note that this routine requires the unsafe compilation flag to be specified and so is commented out by default.
    //      /// </summary>
    //      /// <param name="buffer"></param>
    //      public unsafe void NextBytesUnsafe(byte[] buffer)
    //      {
    //          if(buffer.Length % 8 != 0)
    //              throw new ArgumentException("Buffer length must be divisible by 8", "buffer");
    //
    //          uint x=this.x, y=this.y, z=this.z, w=this.w;
    //          
    //          fixed(byte* pByte0 = buffer)
    //          {
    //              uint* pDWord = (uint*)pByte0;
    //              for(int i=0, len=buffer.Length>>2; i < len; i+=2) 
    //              {
    //                  uint t=(x^(x<<11));
    //                  x=y; y=z; z=w;
    //                  pDWord[i] = w = (w^(w>>19))^(t^(t>>8));
    //
    //                  t=(x^(x<<11));
    //                  x=y; y=z; z=w;
    //                  pDWord[i+1] = w = (w^(w>>19))^(t^(t>>8));
    //              }
    //          }
    //
    //          this.x=x; this.y=y; this.z=z; this.w=w;
    //      }

    #endregion

    #region Public Methods [Methods not present on System.Random]

    /// <summary>
    /// Generates a uint. Values returned are over the full range of a uint, 
    /// uint.MinValue to uint.MaxValue, inclusive.
    /// 
    /// This is the fastest method for generating a single random number because the underlying
    /// random number generator algorithm generates 32 random bits that can be cast directly to 
    /// a uint.
    /// </summary>
    /// <returns></returns>
    public uint NextUInt()
    {
        uint t = (x ^ (x << 11));
        x = y; y = z; z = w;
        return (w = (w ^ (w >> 19)) ^ (t ^ (t >> 8)));
    }

    /// <summary>
    /// Generates a random int over the range 0 to int.MaxValue, inclusive. 
    /// This method differs from Next() only in that the range is 0 to int.MaxValue
    /// and not 0 to int.MaxValue-1.
    /// 
    /// The slight difference in range means this method is slightly faster than Next()
    /// but is not functionally equivalent to System.Random.Next().
    /// </summary>
    /// <returns></returns>
    public int NextInt()
    {
        uint t = (x ^ (x << 11));
        x = y; y = z; z = w;
        return (int)(0x7FFFFFFF & (w = (w ^ (w >> 19)) ^ (t ^ (t >> 8))));
    }


    // Buffer 32 bits in bitBuffer, return 1 at a time, keep track of how many have been returned
    // with bitBufferIdx.
    uint bitBuffer;
    uint bitMask = 1;

    /// <summary>
    /// Generates a single random bit.
    /// This method's performance is improved by generating 32 bits in one operation and storing them
    /// ready for future calls.
    /// </summary>
    /// <returns></returns>
    public bool NextBool()
    {
        if (bitMask == 1)
        {
            // Generate 32 more bits.
            uint t = (x ^ (x << 11));
            x = y; y = z; z = w;
            bitBuffer = w = (w ^ (w >> 19)) ^ (t ^ (t >> 8));

            // Reset the bitMask that tells us which bit to read next.
            bitMask = 0x80000000;
            return (bitBuffer & bitMask) == 0;
        }

        return (bitBuffer & (bitMask >>= 1)) == 0;
    }

    #endregion
}

Then you need to initialize it together with your Random:

Random rnd = new Random(10000);
FastRandom fastRnd = new FastRandom(10000);

And method becomes:

public string RollLargeLookup() {
    return LargeLookup[fastRnd.Next(99999) + 1];
}
Evk
  • 98,527
  • 8
  • 141
  • 191
  • I tested it and yes, the large look up is faster. Upvoted. Although if we use large look up, then there is little algorithm going on already. About the FastRandom, we would probably want to use it as benchmarker. But nice! Large look up is certainly where we want to make use of memory for performance! ;) – Ian May 23 '16 at 06:20
  • Please see the discussion in the Thomas' answer. I am free if you want to put up a solution with FastRandom too, if that has justifiable reason as much as Thomas' answer does. – Ian May 29 '16 at 15:58
  • @Ian do you need something from me to include test with FastRandom? Maybe I should include code of that FastRandom in my answer? As far as I know Xorshift algorithm is not worse than built-in Random in terms of resulting pseudo-random distribution, so not sure what justifiable reason could be to NOT use it (except of relying on built-in implementation of things of course), when your aim is perfomance. – Evk May 29 '16 at 16:12
  • yes, please. If that answer can be tested and showing *faster* result, and also is justifiable in two senses: (1) has a very little bias (it is expected, even if we use `Random` itself there *will* still be *unintentional* bias - but also expected to be *small*), (2) *if* it is tested under the same sequence of number it will *still* be faster (as I mention in the Thomas' answer's comment) then it is *justifiable* - due to the nature of *randomness* we expect. – Ian May 29 '16 at 16:17
  • @Ian added implmenetation of FastRandom class in my answer. – Evk May 29 '16 at 16:38
  • Ok, will test it together with Ivan's new answer tomorrow when I get back to my laptop where I tested all the methods. Thanks. – Ian May 29 '16 at 16:40
  • Any reason why you don't use `NextUInt` for `FastRandom` although it is written in the comment as the "fastest" method? Your current method's result is very close to Thomas' Parameterless Method in my laptop. – Ian May 30 '16 at 03:38
  • Ok, the testing is closed. No new method is accepted for bounty. Please check the result + the way the tests are conducted in my answer, and let me know if you have any objection/things which I miss of (before the bounty grace period expires). If there isn't, then the bounty will be awarded to the solution with the fastest result (as I mention in the bounty comment). Thomas Ayoub's answer is *justifiable* IMO, so does Evk's `FastRandom`. Please read the the explanation given. – Ian May 30 '16 at 04:12
3

Since at this moment the subject has been moved to Random method micro optimizations, I'll concentrate on LargeLookup implementations.

First of, the RollLargeLookupParameterlessRandom solution in addition to bias has another issue. All other implementations check random numbers in range [1, 99999] inclusive, i.e. total 99999 numbers while % 100000 generates range [0, 99999] inclusive, i.e. total 100000 numbers.

So let correct that and at the same time optimize a bit RollLargeLookup implementation by removing add operation:

private string[] LargeLookup;
private void InitLargeLookup()
{
    LargeLookup = new string[99999];
    for (int i = 0; i < LargeLookup.Length; i++)
    {
        LargeLookup[i] = (i + 1) % 100 % 11 == 0 ? "doubles" : "none";
    }
}

public string RollLargeLookup()
{ 
    return LargeLookup[rnd.Next(99999)];
}

public string RollLargeLookupParameterlessRandom()
{ 
    return LargeLookup[rnd.Next() % 99999];
}

Now, can we optimize further the RollLargeLookupParameterlessRandom implementation and at the same time remove the forementioned bias issue and make it compatible with the other implementations? It turns out that we can. In order to do that again we need to know the Random.Next(maxValue) implementation which is something like this:

return (int)((Next() * (1.0 / int.MaxValue)) * maxValue);

Note that 1.0 / int.MaxValue is a constant evaluated at compile time. The idea is to replace 1.0 with maxValue (also constant 99999 in our case), thus eliminating one multiplication. So the resulting function is:

public string RollLargeLookupOptimizedRandom()
{
    return LargeLookup[(int)(rnd.Next() * (99999.0 / int.MaxValue))];
}

Interestingly, this not only fixes the RollLargeLookupParameterlessRandom issues but also is a little bit faster.

Actually this optimization can be applied to any of the other solutions, so the fastest non lookup implementation would be:

public string FastestOptimizedRandomModded()
{
    return ((int)(rnd.Next() * (99999.0 / int.MaxValue)) + 1) % 100 % 11 == 0 ? CONST_DOUBLES : CONST_NONE;
}

But before showing the performance tests, let prove that the result is compatible with Random.Next(maxValue) implementation:

for (int n = 0; n < int.MaxValue; n++)
{
    var n1 = (int)((n * (1.0 / int.MaxValue)) * 99999);
    var n2 = (int)(n * (99999.0 / int.MaxValue));
    Debug.Assert(n1 == n2);
}

Finally, my benchmarks:

64 OS, Release build, Prefer 32 bit = True

Large Lookup Optimized Random: 149 ms
Large Lookup Parameterless Random: 159 ms
Large Lookup: 179 ms
Lookup Optimized Modded: 231 ms
Fastest Optimized Random Modded: 219 ms
Fastest Optimized Modded: 251 ms
Optimized Modded Const: 412 ms
Optimized Modded: 416 ms
Modded: 419 ms
Simple: 1343 ms
Another simple with HashSet: 1805 ms
Another Simple: 2690 ms
Option (Compiled) Regex: 8538 ms
Regex: 14861 ms
EndsWith: 39117 ms

64 OS, Release build, Prefer 32 bit = False

Large Lookup Optimized Random: 121 ms
Large Lookup Parameterless Random: 126 ms
Large Lookup: 156 ms
Lookup Optimized Modded: 168 ms
Fastest Optimized Random Modded: 154 ms
Fastest Optimized Modded: 186 ms
Optimized Modded Const: 178 ms
Optimized Modded: 180 ms
Modded: 202 ms
Simple: 795 ms
Another simple with HashSet: 1287 ms
Another Simple: 2178 ms
Option (Compiled) Regex: 7246 ms
Regex: 17090 ms
EndsWith: 36554 ms
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • Hi Jan, A bit late, but can't miss your party - "and here I go again", you know :) – Ivan Stoev May 29 '16 at 16:04
  • I miss my party? Ah, sorry - you mean *you* can't miss my party. "and here I go again" - ah yes, see you more often in C# bounty question than in others... :p – Ian May 29 '16 at 16:05
  • That being said, the method you use by introducing `99999.0` as constant is a *double* constant I suppose? If that be the case, there *might* still be some bias (very little) because of *double* implementation (which is *inexact* by definition) - is it not? But the bias will be *much* less than 0.0039% - this is what you mean by eliminating the bias - am I right? – Ian May 29 '16 at 16:15
  • I wasn't sure too, so included the *prove* section. The `n1` calc is the unrolled `Random.Next(99999)`, `n2` is mine. – Ivan Stoev May 29 '16 at 16:16
  • Ah, I see what you mean. Probably my bad, I meant eliminating the bias introduced by `%` implementation. Or better just say making it compatible (equivalent) with `Random` class implementation. – Ivan Stoev May 29 '16 at 16:42
  • Ok, the testing is closed. No new method is accepted for bounty. Please check the result + the way the tests are conducted in my answer, and let me know if you have any objection/things which I miss of (before the bounty grace period expires). If there isn't, then the bounty will be awarded to the solution with the fastest result (as I mention in the bounty comment). Thomas Ayoub's answer is *justifiable* IMO, so does Evk's `FastRandom`. Please read the the explanation given. – Ian May 30 '16 at 04:13
  • Fair enough. Two things to mention though. First, I don't see my first method `RollLargeLookupOptimizedRandom` included, did you miss it? Second, I've included my benchmark mainly to give you a hint that the performance of all solutions is different in true 64 bit mode vs 32 bit, so it would be nice if you include similar test in your answer for completeness. – Ivan Stoev May 30 '16 at 08:03
  • ah, I see. I didn't know you want to include `RollLargeLookupOptimizedRandom` too... I thought you only want to include your fastest solution. Yes, I would just simply include your other solution too. It does not seem there is much difference between 64-bit vs 32-bit. But for completeness, then yes, I think it is OK to include the benchmarking for both results. – Ian May 30 '16 at 08:10
  • He-he, actually it was my primary, and the second was just for completeness (applying for both lookup and non lookup categories:)) Anyway, I've just tried the latest, interestingly in 64 bit your current list (1) and (2) are exchanged:) But enough on this, take care. – Ivan Stoev May 30 '16 at 08:27
  • You are right - I almost miss that... (fortunately, I ask each poster to confirm my test first) And yes, yours is the fastest! :) If there is no other valid objection/clarification from other poster(s), the bounty will be yours.. :) and true, I have to take back my words, the "prefer 32-bit" does make a difference. I put the results for both for completeness – Ian May 30 '16 at 08:41
  • Congrats! The bounty is yours! ;) – Ian May 30 '16 at 16:54
  • 1
    Thanks! I had an idea and just waited the contest end. I was thinking to award 100 from my own rep to Gjermund regardless of the contest results, now added also the gain from your bounty. From what I read in the comments under your answer, I think he deserves that. Looking forward for another party by you (not necessarily with bounty:), it was really a fun! – Ivan Stoev May 30 '16 at 17:53
  • 1
    Cool!! :D ah yes, why not? See you in another party! ;) – Ian May 31 '16 at 00:19
3

As several others have already pointed out string comparisons for numbers are not efficient.

    public static String RollNumbers()
    {
        int roll = rnd.Next(1, 100000);

        int lastDigit = roll % 10;
        int secondLastDigit = (roll / 10) % 10;

        if( lastDigit == secondLastDigit )
        {
            return "doubles";
        }
        else
        {
            return "none";
        }
    }

That will run on my machine in 50ms vs the 1200ms of the original approach. Most time is spent on allocating many small temporary objects. If you can you should get rid of strings in the first place. If that is your hot code path it can help to convert your data structures into something which is more expensive to create but very cheap to query. Lookup tables which have been shown here are a good start.

If you look closely to the LargeLookup implementation you will find that most of its good performance is because it does cheat by not using a string as key but it uses the inital random number with some calculations as index. If you try my solution it will most likely run faster because lookup tables tend to have bad cache coherency which makes memory accesses more expensive.

Alois Kraus
  • 13,229
  • 1
  • 38
  • 64
  • Thanks for the answer. Ok, I will test this tomorrow when I get to my laptop where I originally tested all other solutions too. Please expect the bounty to be given during the grace period (according to the test results) - and I appreciate all your efforts to answer. You got my upvote. – Ian May 29 '16 at 17:24
  • Thanks Ian for profiling all solutions. Elapsed time benchmarks are fine but other resource metrics can also be interesting. Personally I have stuck to ETW profiling which gives me by far most insights. See http://etwcontroler.codeplex.com/ for more infos. – Alois Kraus May 29 '16 at 17:40
  • ah, that's indeed an interesting tool for profiling (and is new for me)... Sure, will look at it too. (Though as long as this question and bounty are concerned, it might be a little too late for me to know this tool now. Yet, I will still take a look on it for my personal understanding. Thanks again) – Ian May 29 '16 at 18:03
  • Ok, the testing is closed. No new method is accepted for bounty. Please check the result + the way the tests are conducted in my answer, and let me know if you have any objection/things which I miss of (before the bounty grace period expires). If there isn't, then the bounty will be awarded to the solution with the fastest result (as I mention in the bounty comment). Thomas Ayoub's answer is *justifiable* IMO, so does Evk's `FastRandom`. Please read the the explanation given. – Ian May 30 '16 at 04:12
  • Looks like the others were still faster by sparing the expensive division operation in favor of modulos. If the lookup table is not too big and can fit into the L3 cache (ca. 6 MB) or even the L1/L2 cache then the actual cycles spent in calculating becomes relevant. – Alois Kraus May 30 '16 at 06:45
  • True, it seems like the modulo operation is less expensive than the division (though it is `integer` division - not `double`/floating point). – Ian May 30 '16 at 07:07
2

The fastest that I was able to achieve is optimizing the use of Random with the large lookup method:

return LargeLookup[rnd.Next() % 100000];

And it runs 20% faster than the original since it avoid a division (look at Next() code vs Next(int maxValue)).


Looking for real fairnessIMHO, I changed a bit the way the method where tested.

TL;DR; here's the dasboard:

|-----------------Name---------------|--Avg--|--Min--|---Max---|
|------------------------------------|-------|-------|---------|
|RollLargeLookup                     |    108|    122|    110,2|
|RollLookupOptimizedModded           |    141|    156|    145,5|
|RollOptimizedModdedConst            |    156|    159|    156,7|
|RollOptimizedModded                 |    158|    163|    159,8|
|RollNumbers                         |    197|    214|    200,9|
|RollSimple                          |  1 242|  1 304|  1 260,8|
|RollSimpleHashSet                   |  1 635|  1 774|  1 664,6|
|RollAnotherSimple                   |  2 544|  2 732|  2 603,2|
|RollOptionRegex                     |  9 137|  9 605|  9 300,6|
|RollRegex                           | 17 510| 18 873| 17 959  |
|RollEndsWith                        | 20 725| 22 001| 21 196,1|

I changed a few points:

  • Pre-computed the numbers to test so each method were tested with the same set of numbers (taking out the random generation war and the biais I introduced);
  • Runned each method 10 times in a random order;
  • Introduced a parameter in each function;
  • Removed the dupes.

I created a class MethodToTest:

public class MethodToTest
{
    public delegate string RollDelegate(int number);

    public RollDelegate MethodDelegate { get; set; }
    public List<long> timeSpent { get; set; }

    public MethodToTest()
    {
        timeSpent = new List<long>();
    }

    public string TimeStats()
    {
        return string.Format("Min: {0}ms, Max: {1}ms, Avg: {2}ms", timeSpent.Min(), timeSpent.Max(),
            timeSpent.Average());
    }
}

Here's the main content:

private static void Test()
{
    List<MethodToTest> methodList = new List<MethodToTest>
    {
        new MethodToTest{ MethodDelegate = RollNumbers},
        new MethodToTest{ MethodDelegate = RollLargeLookup},
        new MethodToTest{ MethodDelegate = RollLookupOptimizedModded},
        new MethodToTest{ MethodDelegate = RollOptimizedModdedConst},
        new MethodToTest{ MethodDelegate = RollOptimizedModded},
        new MethodToTest{ MethodDelegate = RollSimple},
        new MethodToTest{ MethodDelegate = RollSimpleHashSet},
        new MethodToTest{ MethodDelegate = RollAnotherSimple},
        new MethodToTest{ MethodDelegate = RollOptionRegex},
        new MethodToTest{ MethodDelegate = RollRegex},
        new MethodToTest{ MethodDelegate = RollEndsWith},
    };

    InitLargeLookup();
    Stopwatch s = new Stopwatch();
    Random rnd = new Random();
    List<int> Randoms = new List<int>();

    const int trial = 10000000;
    const int numberOfLoop = 10;
    for (int j = 0; j < numberOfLoop; j++)
    {
        Console.Out.WriteLine("Loop: " + j);
        Randoms.Clear();
        for (int i = 0; i < trial; ++i)
            Randoms.Add(rnd.Next(1, 100000));

        // Shuffle order
        foreach (MethodToTest method in methodList.OrderBy(m => new Random().Next()))
        {
            s.Restart();
            for (int i = 0; i < trial; ++i)
                method.MethodDelegate(Randoms[i]);

            method.timeSpent.Add(s.ElapsedMilliseconds);
            Console.Out.WriteLine("\tMethod: " +method.MethodDelegate.Method.Name);
        }
    }

    File.WriteAllLines(@"C:\Users\me\Desktop\out.txt", methodList.OrderBy(m => m.timeSpent.Average()).Select(method => method.MethodDelegate.Method.Name + ": " + method.TimeStats()));
}

And here are the functions:

//OP's Solution 2
public static String RollRegex(int number)
{
    return Regex.IsMatch(number.ToString(), @"(.)\1{1,}$") ? "doubles" : "none";
}

//Radin Gospodinov's Solution
static readonly Regex OptionRegex = new Regex(@"(.)\1{1,}$", RegexOptions.Compiled);
public static String RollOptionRegex(int number)
{
    return OptionRegex.IsMatch(number.ToString()) ? "doubles" : "none";
}

//OP's Solution 1
public static String RollEndsWith(int number)
{
    if (number.ToString().EndsWith("11") || number.ToString().EndsWith("22") || number.ToString().EndsWith("33") ||
        number.ToString().EndsWith("44") || number.ToString().EndsWith("55") || number.ToString().EndsWith("66") ||
        number.ToString().EndsWith("77") || number.ToString().EndsWith("88") || number.ToString().EndsWith("99") ||
        number.ToString().EndsWith("00"))
    {
        return "doubles";
    }
    return "none";
}

//Ian's Solution   
public static String RollSimple(int number)
{
    string rollString = number.ToString();
    return number > 10 && rollString[rollString.Length - 1] == rollString[rollString.Length - 2] ?
        "doubles" : "none";
}

//Ian's Other Solution
static List<string> doubles = new List<string>() { "00", "11", "22", "33", "44", "55", "66", "77", "88", "99" };
public static String RollAnotherSimple(int number)
{

    string rollString = number.ToString();
    return rollString.Length > 1 && doubles.Contains(rollString.Substring(rollString.Length - 2)) ?
        "doubles" : "none";
}

//Dandré's Solution
static HashSet<string> doublesHashset = new HashSet<string>() { "00", "11", "22", "33", "44", "55", "66", "77", "88", "99" };
public static String RollSimpleHashSet(int number)
{
    string rollString = number.ToString();
    return rollString.Length > 1 && doublesHashset.Contains(rollString.Substring(rollString.Length - 2)) ?
        "doubles" : "none";
}


//Stian Standahl optimizes modded solution
public static string RollOptimizedModded(int number) { return number % 100 % 11 == 0 ? "doubles" : "none"; }

//Gjermund Grøneng's method with constant addition
private const string CONST_DOUBLES = "doubles";
private const string CONST_NONE = "none";
public static string RollOptimizedModdedConst(int number) { return number % 100 % 11 == 0 ? CONST_DOUBLES : CONST_NONE; }

//Corak's Solution, added on Gjermund Grøneng's
private static readonly string[] Lookup = { "doubles", "none", "none", "none", "none", "none", "none", "none", "none", "none", "none" };
public static string RollLookupOptimizedModded(int number) { return Lookup[number % 100 % 11]; }

//Evk's Solution, large Lookup
private static string[] LargeLookup;
private static void InitLargeLookup()
{
    LargeLookup = new string[100000];
    for (int i = 0; i < 100000; i++)
    {
        LargeLookup[i] = i % 100 % 11 == 0 ? "doubles" : "none";
    }
}
public static string RollLargeLookup(int number) { return LargeLookup[number]; }

//Alois Kraus's Solution
public static string RollNumbers(int number)
{
    int lastDigit = number % 10;
    int secondLastDigit = (number / 10) % 10;

    return lastDigit == secondLastDigit ? "doubles" : "none";
}
Thomas Ayoub
  • 29,063
  • 15
  • 95
  • 142
  • @Ian Well, I might have missed that. But it doesn't change the result :) – Thomas Ayoub May 25 '16 at 12:44
  • Ok, your solution seems to work great! I tested it with different laptop though - But I can see the performance increases significantly! Will test it and edit my answer with the profile result under the same condition as soon as I am back to my original laptop! Upvoted. ;) – Ian May 25 '16 at 12:49
  • Tested and the result presented. Yep, it increases by 20%! ;) – Ian May 26 '16 at 03:13
  • I don't think that this solution is equivalent to the original one. If you use a known seed, do you get the same exact sequence? – Phil1970 May 27 '16 at 18:37
  • @Phil1970 just look at the source, you'll see that it's the exact same, unless that I do fewer operation – Thomas Ayoub May 27 '16 at 19:25
  • 4
    `rnd.Next(100000)` returns values between 0 and 99999 with equal probability, but `rnd.Next() % 100000` **DOES NOT**. That's because `rnd.Next()` returns values between 0 and 2147483647 with equal probability, so taking just the last five digits means that values between 0 and 83647 occur slightly more often than values between 83648 and 99999. – Michael Liu May 28 '16 at 02:40
  • @MichaelLiu You may believe in true randomness, I don't. The number distribution from `0` to `2147400000` represent `99,9961%` of the range from 0 to `Int32.MaxValue`. Which means that the numbers from 0 to 83647 will appear `0,0039%` more times than values between 83648 and 99999. – Thomas Ayoub May 28 '16 at 09:42
  • 1
    Here's a [very nice analysis of this bias](https://ericlippert.com/2013/12/16/how-much-bias-is-introduced-by-the-remainder-technique/) by Eric Lippert, read it if you intend to use that thing in real code. – Lucas Trzesniewski May 28 '16 at 10:34
  • @LucasTrzesniewski thanks for the link, I agree that a bias of 5% is quite big, but since .Net random use 32bits int, the bias is only of 0,0039% which is, to me, acceptable in most of the use cases – Thomas Ayoub May 28 '16 at 11:04
  • 1
    @ThomasAyoub What I want to say is that if you want to use a know seed (maybe for unit testing) as in `var rnd = new Random(12345)` then the final sequence might not be the same if modulo (or + 1) is used when calling Next. For comparison to be fair, you should get the exact same sequence from the same seed. – Phil1970 May 28 '16 at 20:59
  • @Phil1970 the comparison is already highly unfair, since none of the lookup table method include the seeding of the table, some compute values while other search for. The more run you'll give it, the more the lookup tables will *seems* fast, since it's only memory access. But if you run the tests once (and include the table seeding), the faster method is the one using `x % 100 % 11` – Thomas Ayoub May 28 '16 at 21:43
  • I just put up what I thought of this solution in my answer. I think my assessment is fair enough for your case, @Phil1970 please look at my answer if you want to add some other points regarding this answer which I might not be aware of - I appreciate all your discussion here. – Ian May 29 '16 at 04:32
  • @MichaelLiu I just put up what I thought of this solution in my answer. I think my assessment is fair enough for Thomas Ayoub's' case, please look at my answer if you want to add some other points regarding this answer which I might not be aware of - I appreciate all your discussion here. It will help me to decide to give the bounty with fuller consideration of the matter from all sides. – Ian May 29 '16 at 04:34
  • @LucasTrzesniewski I just put up what I thought of this solution in my answer. I think my assessment is fair enough for Thomas Ayoub's' case, please look at my answer if you want to add some other points regarding this answer which I might not be aware of - I appreciate all your discussion here. It will help me to decide to give the bounty with fuller consideration of the matter from all sides. – Ian May 29 '16 at 04:34
  • @Ian well, the problem with your benchmark is that it includes the call to `Random.Next` in the measured time. On my PC, your benchmark runs Thomas' code in ~209ms. A function which does `rnd.Next(99999);return "lol";` runs in 217ms, but `rnd.Next();return "lol";` runs in 188ms. We may say you're effectively benchmarking the RNG here. May I enter the competition by switching to [this RNG](https://xkcd.com/221/)? :-) – Lucas Trzesniewski May 29 '16 at 10:11
  • @LucasTrzesniewski lol... Yes, you are welcome to enter the competition too... Except that your RNG link will certainly far too simple and does not create random at all. :P I understand that Thomas' answer sparks this dicussion because he gives answer which cannot be tested together with the rests and having little bias, yet produces the fastest solution. I don't want to be unfair here, but I also don't want to be overly strict. If I can see justifying reason why the very low bias is acceptable and the randomness is still retained (though not of the same sequence) I am open to that too – Ian May 29 '16 at 15:44
  • @LucasTrzesniewski if you see the answer I put, I also say that "even the same number of sequence is generated, Thomas' method would still be faster" - because of the comparison between Next() and Next(int) internal methods. So, this is the kind of "justifying reason" which I need... Of course, it is also true that this makes the comparison changed. And thus, Evk's solution to include FastRandom is now opened too.. so even Thomas' method is now the fastest, if that possibility is opened and Evk's solution mentions that, I will put the Evk's solution into possibility too... – Ian May 29 '16 at 15:47
  • @LucasTrzesniewski I will put what Evk's mention using `FastRandom` into consideration* too... – Ian May 29 '16 at 16:04
  • Ok, the testing is closed. No new method is accepted for bounty. Please check the result + the way the tests are conducted in my answer, and let me know if you have any objection/things which I miss of (before the bounty grace period expires). If there isn't, then the bounty will be awarded to the solution with the fastest result (as I mention in the bounty comment). Thomas Ayoub's answer is *justifiable* IMO, so does Evk's `FastRandom`. Please read the the explanation given. – Ian May 30 '16 at 04:13
  • @ThomasAyoub OK, will take a look on that... But.. it *seems* to be a new method, is it? – Ian May 30 '16 at 11:06
  • @Ian it's a new testing method (I didn't include new computation method) which reflect more irl needs. [I think] – Thomas Ayoub May 30 '16 at 11:07
  • @ThomasAyoub OK, will take a look on that. – Ian May 30 '16 at 11:08
  • @ThomasAyoub why do some of the methods (like Ivan's and Evk's FastRandom) are not tested in the proposed methods? Do you happen to miss them? Or you just want me to test them? – Ian May 30 '16 at 11:21
  • I didn't include mine neither because introducing the parameter within the function made them duplicate of already existing functions (since we were just playing on *how* the random number was generated) @Ian – Thomas Ayoub May 30 '16 at 11:23
  • I see... That's why some of the methods are not compatible for being tested with this method. I think I understand it. This looks like more standardized test method for me - with the expense of some possible (creative) solutions but not following certain standard. Actually, I am OK with some bias and non-100% accurate results so long it is *justifiable*. Such are the answers I am looking for, since I try to solve more of *engineering* problem than *scientific* (hope I use the words correctly here :)) and for the same reason I defended your solution as *justifiable* though not standardized b4. – Ian May 30 '16 at 15:34
  • It (your solution) has pretty good result with small bias. And non-exact same sequence is OK because by looking at how Next vs Next(int) implemented, it is not unjustifiable to say that your solution will still be faster even the same number sequence is used. But if we use the testing method you are proposing now, I am afraid that some creative solutions (such as faster generation of Random) must be thrown away. Good as it is, I think the testing method is not as suitable for the purpose of this post. Still, it is nice to know that though. :) – Ian May 30 '16 at 15:38