Не могли бы вы рассмотреть мою реализацию Quick Int Parser?

Я написал несколько полезных методов, которые могут анализировать 32-разрядное целое число со знаком намного быстрее, чем Int32.Parse. Надеюсь, вы могли бы дать мне некоторый опыт, просмотрев мой код и предложив улучшения или указав на ошибки. Если вам интересно (и у вас есть время, конечно), я был бы очень благодарен.

Я разместил свой код на веб-сайте "Refactor My Code". Ссылка здесь: http://refactormycode.com/codes/699-int32-quick-parser

Спасибо.


Изменить: Спасибо всем за полезные предложения. Я следил за вашими идеями, и вот обновленная версия: http://refactormycode.com/codes/699-int32-quick-parser#refactor_142758

(Пожалуйста, используйте утилиту diff, чтобы увидеть измененные строки кода.)

Спасибо!

Ответы

Ответ 1

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

Результаты (выполняется 30 выборок 10 М случайных целых строк):

Int32.Parse                      1766.17 ms (stddev: 1.83 ms)
Numbers.Int32.ParseTrusted       262.07 ms (stddev: 0.44 ms)
ParseTrustedUnsafe               146.13 ms (stddev: 0.43 ms)

вот моя небезопасная реализация:

public static unsafe int ParseTrustedUnsafe(string str, int start, int end)
{
    unsafe
    {
        Int32 result = 0;
        Int32 length = end - start;
        Boolean isNegative = false;
        fixed (Char* startChar = str)
        {
            Byte* currentChar = ((Byte*)startChar) + (start * 2);
            if (*currentChar == 0x2D)
            {
                isNegative = true;
                currentChar += 2;
                length--;
            }
            else if (*currentChar == 0x2B)
            {
                currentChar += 2;
                length--;
            }
            while (length >= 4)
            {
                result = (result * 10) + (*currentChar - 0x30);
                result = (result * 10) + (*(currentChar + 2) - 0x30);
                result = (result * 10) + (*(currentChar + 4) - 0x30);
                result = (result * 10) + (*(currentChar + 6) - 0x30);

                currentChar += 8;
                length -= 4;
            }
            while (length > 0)
            {
                result = (result * 10) + (*currentChar - 0x30);

                currentChar += 2;
                length -= 1;
            }
        }
        return isNegative ? -result : result;
    }
}

Ниже приведена моя программа проверки и проверки:

static void Main(string[] args)
{
    String[] testValues = new String[10 * 100 * 100 * 100];
    Random rand = new Random();
    for (int i = 0; i < testValues.Length; i++)
    {
        testValues[i] = rand.Next().ToString();
    }
    // validate
    for (int i = 0; i < testValues.Length; i++)
    {
        Debug.Assert(String.CompareOrdinal(testValues[i], ParseTrustedUnsafe(testValues[i], 0, testValues[i].Length).ToString()) == 0, "ParseTrustedUnsafe failed on " + testValues[i]);
        Debug.Assert(String.CompareOrdinal(testValues[i], Numbers.Int32.ParseTrusted(testValues[i], 0, testValues[i].Length).ToString()) == 0, "Numbers.Int32.ParseTrusted failed on " + testValues[i]);
    }
#if DEBUG
    return;
#endif    
    List<List<Double>> results = new List<List<double>>();
    for (int i = 0; i < 3; i++)
    {
        results.Add(new List<double>());
    }

    Thread.CurrentThread.Priority = ThreadPriority.Highest;
    Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.RealTime;
    if (Environment.ProcessorCount > 1)
    {
        Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(1 << (Environment.ProcessorCount - 1)); // use last core only
    }
    Stopwatch sw = new Stopwatch();
    for (int testrun = 0; testrun < 30; testrun++)
    {
        sw.Reset();
        sw.Start();
        for (int i = 0; i < testValues.Length; i++)
        {
            Int32.Parse(testValues[i]);
        }
        sw.Stop();
        results[0].Add(sw.ElapsedMilliseconds);

        sw.Reset();
        sw.Start();
        for (int i = 0; i < testValues.Length; i++)
        {
            Numbers.Int32.ParseTrusted(testValues[i]);
        }
        sw.Stop();
        results[1].Add(sw.ElapsedMilliseconds);

        sw.Reset();
        sw.Start();
        for (int i = 0; i < testValues.Length; i++)
        {
            ParseTrustedUnsafe(testValues[i], 0, testValues[0].Length);
        }
        sw.Stop();
        results[2].Add(sw.ElapsedMilliseconds);

    }
    Console.WriteLine("Int32.Parse \t\t\t {0:0.00}ms (stddev: {1:0.00}ms)", results[0].Average(), StandardDeviation(results[0]));
    Console.WriteLine("Numbers.Int32.ParseTrusted \t {0:0.00}ms (stddev: {1:0.00}ms)", results[1].Average(), StandardDeviation(results[1]));
    Console.WriteLine("ParseTrustedUnsafe \t\t {0:0.00}ms (stddev: {1:0.00}ms)", results[2].Average(), StandardDeviation(results[2]));
}

private static double StandardDeviation(IEnumerable<double> doubleList)
{
    double average = doubleList.Average();
    double sumOfDerivation = 0;
    foreach (double value in doubleList)
    {
        sumOfDerivation += (value) * (value);
    }
    double sumOfDerivationAverage = sumOfDerivation / doubleList.Count();
    return Math.Sqrt(sumOfDerivationAverage - (average * average));
}

Ответ 2

Один, хотя; рассмотрите возможность переименования класса; вызов Int32 просто вызывает проблемы, хотя здесь вы получаете немного места, потому что он вложен (Numbers.Int32). Я бы не стал слишком взволнован этим, хотя (вложенность действительно избавляет от боли для потребителя).

Не вызывайте аргументы вроде str - это не имеет смысла. Тем не менее, int.Parse называет его s, поэтому я не могу быть слишком критичным.

Но я должен предоставить вам, показатель производительности 10x неплох; -p (хотя он, конечно, поддерживает только один формат).

Еще одна вещь; начало и длина - более распространенный шаблон, чем начало и конец.

Наконец, есть ли в настройках ParseSimple вне очереди?

if (end > str.Length) throw new ArgumentOutOfRangeException("end");

Не должно быть > =? Если длина равна 1, то конец может быть равен 0?

Ответ 3

Одна причудливая мысль: вместо того, чтобы вычитать "0" на каждой итерации, вы можете получить предварительно рассчитанную сумму для вычитания в самом конце, в зависимости от количества цифр. (длина = 1, вычесть '0', length = 2, вычесть ('0' * 10 + '0')). Возможно, вам удастся избежать этого только для относительно коротких строк, хотя из-за переполнения. (Или используйте длинный, но это может замедлить его слишком.)

Кроме того, я не пробовал, но вы можете проверить,

result = (result << 3) + result + result + [...]

или

result = (result << 3) + (result << 1) + [...]

быстрее, чем

result = result*10 + [...]

Я подозреваю, что этого не будет, но, возможно, стоит хотя бы попробовать. (Конечно, битверение происходит быстрее, чем умножение, но я подозреваю, что дополнительные дополнения связаны с уравновешиванием этого.)

Профилировали ли вы код, чтобы увидеть, где существуют узкие места?

Есть ли у вас какие-либо результаты тестов, насколько это быстрее, чем Int32.Parse, btw?

Ответ 4

Итак, если у вас есть parse trusted, то, возможно, вам стоит рассмотреть возможность использования ParsePositive ParseNegative

И вы должны смотреть IL, редактируя свой код