Подтвердить что ты не робот

Какой из них быстрее? Regex или EndsWith?

Что будет быстрее?

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

или

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";
}

В настоящее время я использую действительно длинный список операторов if, полный с регулярными выражениями, чтобы проверить, заканчивается ли int с удвоениями, тройками, квадратиками, квинтами и т.д.... поэтому я хотел бы знать, какой из них быстрее, прежде чем я изменюсь все это.

4b9b3361

Ответ 1

В вашем конкретном случае Regex на самом деле быстрее... но это, скорее всего, потому, что вы используете EndsWith со многими OR и избыточными ToString(). Если вы упростите логику, простая операция string, скорее всего, будет быстрее.


Вот краткое описание производительности для обработки текста - от самого быстрого до самого медленного (10-миллиметровый цикл [Предпочитаемый/Непоглощающий 32-разрядный] - ранжирование упорядочивается на основе самого быстрого из двух):

  • Большой поиск Быстрый Случайный UInt (не подсчитывается за щедрость): 219/273 мс - Шахта, улучшенная от Evk
  • Оптимизированный поиск большого размера Случайный: 228/273 мс - Альтернативное решение Ivan Stoev
  • Большой поиск Быстрый Случайный: 238/294 мс - Альтернативное решение Evk
  • Большой поиск Параметрический случайный: 248/287 мс - Thomas Ayoub

    В этом решении есть несколько заметок (на основе комментариев ниже):

    • Это решение вводит смещение 0.0039% к небольшим числам (< 100000) (ссылка: сообщение в блоге Eric Lippert, связанное Лукас Трежневски)
    • Не генерирует ту же последовательность чисел, что и другие во время тестирования (ref: Michael Liu comment) - поскольку он изменяет способ использования Random (от Random.Next(int) до Random.Next()), который используется для самого тестирования.

    В то время как тестирование не может быть выполнено с той же последовательностью чисел для этого метода, что и для остатков (в качестве упомянутых Phil1970), у меня есть два момента:

    • Некоторым может быть интересно посмотреть на реализацию Random.Next() vs Random.Next(int), чтобы понять, почему это решение будет по-прежнему быстрее , даже если используется одна и та же последовательность чисел.
    • Использование Random в самом реальном случае будет (в большинстве случаев) не предполагать, что порядковая номер будет одинаковой (или предсказуемой). Только для тестирования нашего метода мы хотим, чтобы последовательность Random быть идентичными (для целей честного тестирования). Ожидаемый более быстрый результат для этого метода не может быть полностью получен только из результата тестирования, но также рассматривается реализация Next() vs Next(int).
  • Большой поиск: 320/284 мс - Evk

  • Самый быстрый оптимизированный случайный модем: 286/333 мс Иван Стоев
  • Оптимизированный поиск: 315/329 мс - Corak
  • Оптимизированный модем: 471/330 мс - Stian Standahl
  • Оптимизированный Модифицированный + Постоянный: 472/337 - Gjermund Grøneng
  • Самый быстрый оптимизированный модем: 345/340 мс - Gjermund Grøneng
  • Modded: 496/370 ms - Corak + возможно Алексей Левенков
  • Числа: 537/408 мс - Алоис Краус
  • Простой: 1668/1176 мс - Шахта
  • HashSet Содержит: 2138/1609 мс - Dandré
  • Список содержит: 3013/2465 мс - Другая шахта
  • Скомпилированное Regex: 8956/7675 ms - Радин Господинов
  • Regex: 15032/16640 ms - Решение 1
  • EndsWith: 24763/20702 ms - OP Solution 2

Вот мои простые тестовые примеры:

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

//OP 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 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 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é 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 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 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 method after optimizing the Random (The fastest!)
public string FastestOptimizedModded() { return (rnd.Next(99999) + 1) % 100 % 11 == 0 ? CONST_DOUBLES : CONST_NONE; }

//Corak 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 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 Solution
public string RollLargeLookupParameterlessRandom() { return LargeLookup[rnd.Next() % 100000]; }

//Alois Kraus 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 Solution
public string FastestOptimizedRandomModded() {
    return ((int)(rnd.Next() * (99999.0 / int.MaxValue)) + 1) % 100 % 11 == 0 ? CONST_DOUBLES : CONST_NONE;
}

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

//Evk 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];
}

Дополнительный класс FastRandom:

//FastRandom 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 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
}

Сценарий тестирования:

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());
    }
}

Результат (предпочтительно 32-бит):

[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

Результат (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

И изображение:

введите описание изображения здесь

Ответ 2

@StianStandahls друг здесь. Это решение быстрее! Это то же самое, что и предыдущий самый быстрый пример в ответе @Ians, но здесь оптимизирован случайный генератор.

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;
}

Ответ 3

Что касается самой высокой производительности, я считаю, что @Ian уже освещал это довольно хорошо. Все кредиты идут к нему.

Одна вещь, на которую не отвечает мое удовлетворение в Q/A, - это то, почему Regex'es превосходят EndsWith в этом случае. Я почувствовал необходимость объяснить разницу, чтобы люди поняли, какое решение, вероятно, будет работать лучше в этом сценарии.

EndsWith

Функциональность EndsWith в основном представляет собой "сравнение" на части строки в последовательном порядке. Что-то вроде этого:

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;
}

Код довольно прямой; мы просто берем первый символ, видим, совпадает ли он, затем следующий и т.д. - пока мы не нажмем конец строки.

Regex

Regex'es работают по-разному. Подумайте, ищите иглу "foofoo" в очень большой стоге сена. Очевидная реализация заключается в первом символе, проверьте, является ли это "f", перейдите к следующему символу и т.д., Пока мы не нажмем конец строки. Однако мы можем сделать гораздо лучше:

Посмотрите внимательно на задачу. Если мы сначала посмотрим на символ 5 строки и заметим, что это не "o" (последний символ), мы можем сразу перейти к символу 11 и снова проверить, является ли это "o". Таким образом, мы получим хорошее улучшение по сравнению с нашим исходным кодом фактора 6 в лучшем случае и с той же производительностью в худшем случае.

Также обратите внимание, что регулярные выражения могут усложняться с помощью "или", "и" и т.д. Выполнение передовых сканирования больше не имеет большого смысла, если нам нужно только посмотреть на завершающие символы.

Вот почему Regex's обычно работают с NFA, которые скомпилированы в DFA. Здесь есть отличный онлайн-инструмент: http://hackingoff.com/compilers/regular-expression-to-nfa-dfa, который показывает, как это выглядит (для простых регулярных выражений).

Внутри вы можете попросить .NET скомпилировать Regex с помощью Reflection.Emit, и когда вы используете регулярное выражение, вы фактически оцениваете этот оптимизированный скомпилированный конечный автомат (RegexOptions.Compiled).

То, что вы, вероятно, закончите, - это то, что работает следующим образом:

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;
}

Итак, что быстрее?

Ну, это зависит. Там накладные расходы при определении NFA/DFA из выражения, компиляции программы и для каждого вызова, ищущего программу и оценивающей ее. Для очень простых случаев EndsWith превосходит Regex. В этом случае "OR" в EndsWith делает его медленнее, чем Regex.

С другой стороны, регулярное выражение обычно используется вами несколько раз, а это значит, что вам нужно только его компилировать один раз и просто искать его для каждого вызова.

Ответ 4

Немного больше возможностей можно было бы выжать, если бы предвидеть всю таблицу поиска для всех возможных значений. Это позволит избежать двух делений по модулю в самом быстром методе, поэтому будет немного быстрее:

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";
    }
}

И сам метод тогда справедлив:

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

В то время как вы выглядите несколько убедительными - такие методы часто используются. Например, самый быстрый известный оценщик покера в покере преподает огромный массив с множеством записей (с очень умными трюками), а затем просто делает несколько простых поисков в этом массиве, чтобы мгновенно оценить силу одной покера за другую.

Вы можете сделать еще быстрее, используя альтернативные генераторы случайных чисел. Например, если вы замените System.Random на this реализацию класса FastRandom (на основе алгоритма xorshift) - он будет в два раза быстрее.

Если вы используете как большую таблицу поиска, так и FastRandom - на моем компьютере он показывает 100 мс против 220 мс RollLookupOptimizedModded.

Вот исходный код класса FastRandom, упомянутый в моей ссылке выше:

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 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
}

Затем вам нужно инициализировать его вместе со своим Random:

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

И метод становится:

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

Ответ 5

Поскольку в этот момент объект был перенесен на микро-оптимизацию метода Random, я сосредоточусь на реализациях LargeLookup.

Во-первых, решение RollLargeLookupParameterlessRandom в дополнение к смещению имеет еще одну проблему. Все остальные реализации проверяют случайные числа в диапазоне [1, 99999] включительно, т.е. Всего 99999 номеров, а % 100000 генерирует диапазон [0, 99999] включительно, т.е. Всего 100000 номеров.

Итак, давайте исправьте это и в то же время оптимизируем реализацию бит RollLargeLookup, удалив операцию добавления:

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];
}

Теперь, можем ли мы оптимизировать реализацию RollLargeLookupParameterlessRandom и в то же время удалить проблему с предустановленным смещением и сделать ее совместимой с другими реализациями? Оказывается, мы можем. Чтобы сделать это снова, нам нужно знать реализацию Random.Next(maxValue), которая выглядит примерно так:

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

Обратите внимание, что 1.0 / int.MaxValue является константой, оцененной во время компиляции. Идея состоит в том, чтобы заменить 1.0 на maxValue (также постоянный 99999 в нашем случае), тем самым исключая одно умножение. Таким образом, результирующая функция:

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

Интересно, что это не только устраняет проблемы RollLargeLookupParameterlessRandom, но и немного быстрее.

На самом деле эта оптимизация может быть применена к любому из других решений, поэтому самая быстрая реализация без поиска:

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

Но прежде чем показывать тесты производительности, докажите, что результат совместим с реализацией Random.Next(maxValue):

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);
}

Наконец, мои тесты:

64 ОС, выпуск сборки, предпочтительный 32 бит = 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 ОС, выпуск сборки, Предпочитаете 32 бит = 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

Ответ 6

Как уже отмечалось, несколько строк сравнения строк для чисел неэффективны.

    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";
        }
    }

Это будет работать на моей машине в 50 мс против 1200 мс исходного подхода. Большинство времени тратится на выделение многих небольших временных объектов. Если вы можете, вы должны избавиться от струн в первую очередь. Если это ваш путь к горячим кодам, это может помочь преобразовать ваши структуры данных в нечто более дорогое, но очень дешево для запроса. Таблицы поиска, которые были показаны здесь, являются хорошим началом.

Если вы посмотрите внимательно на реализацию LargeLookup, вы обнаружите, что большая часть его хорошей производительности связана с тем, что она обманывает, не используя строку как ключ, но она использует случайное число с некоторыми вычислениями в качестве индекса. Если вы попробуете мое решение, оно скорее всего будет работать быстрее, потому что таблицы поиска имеют тенденцию к плохой кэш-когерентности, что делает доступ к памяти более дорогим.

Ответ 7

Самый быстрый, который я смог достичь, - это оптимизация использования Random с помощью большого метода поиска:

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

И он работает на 20% быстрее, чем оригинал, так как он избегает деления (посмотрите Next() code vs Далее (int maxValue)).


Ищем реальную справедливость IMHO я немного изменил способ тестирования.

TL; DR; здесь dscboard:

|-----------------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|

Я изменил несколько моментов:

  • Предварительно вычислено число, чтобы проверить, чтобы каждый метод тестировался с одинаковым набором чисел (извлечение войны с случайной генерацией и введенной мной biais);
  • Выполняет каждый метод 10 раз в произвольном порядке;
  • Введен параметр в каждую функцию;
  • Удалены обманы.

Я создал класс 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());
    }
}

Здесь основное содержание:

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()));
}

И вот функции:

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

//Radin Gospodinov 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 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 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 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é 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 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 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 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 Solution
public static string RollNumbers(int number)
{
    int lastDigit = number % 10;
    int secondLastDigit = (number / 10) % 10;

    return lastDigit == secondLastDigit ? "doubles" : "none";
}