Int, короткая, байт-производительность в обратном порядке для циклов

(background: Почему я должен использовать int вместо байта или short в С#)

Чтобы удовлетворить мое любопытство в отношении плюсов и минусов использования целого числа "соответствующего размера" и "оптимизированного" целого числа, я написал следующий код, который усилил то, что я ранее считал истинным в отношении производительности int в .Net(и который объясняется в ссылке выше), которая заключается в том, что она оптимизирована для производительности int, а не короткой или байтовой.

DateTime t;
long a, b, c;

t = DateTime.Now;
for (int index = 0; index < 127; index++)
{
    Console.WriteLine(index.ToString());
}           
a = DateTime.Now.Ticks - t.Ticks;

t = DateTime.Now;
for (short index = 0; index < 127; index++)
{
    Console.WriteLine(index.ToString());
}

b=DateTime.Now.Ticks - t.Ticks;

t = DateTime.Now;           
for (byte index = 0; index < 127; index++)
{
    Console.WriteLine(index.ToString());
}
c=DateTime.Now.Ticks - t.Ticks;

Console.WriteLine(a.ToString());
Console.WriteLine(b.ToString());
Console.WriteLine(c.ToString());

Это дает примерно согласованные результаты в области...

~ 950000

~ 2000000

~ 1700000

Это соответствует тому, что я ожидаю увидеть.

Однако, когда я пытаюсь повторить петли для каждого типа данных, как это...

t = DateTime.Now;
for (int index = 0; index < 127; index++)
{
    Console.WriteLine(index.ToString());
}
for (int index = 0; index < 127; index++)
{
    Console.WriteLine(index.ToString());
}
for (int index = 0; index < 127; index++)
{
    Console.WriteLine(index.ToString());
}
a = DateTime.Now.Ticks - t.Ticks;

Цифры больше похожи...

~ 4500000

~ 3100000

~ 300000

Что я нахожу загадочным. Может кто-нибудь предложить объяснение?

Примечание: В интересах сравнения, как, например, я ограничил цикл до 127 из-за диапазона типа байта. Также это акт любопытства, а не производственная кодовая микро-оптимизация.

Ответы

Ответ 1

Прежде всего, это не .NET, оптимизированный для производительности int, это машина, которая оптимизирована, потому что 32 бита - это собственный размер слова (если вы не на x64, и в этом случае it long или 64 бит).

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

В-третьих, a byte имеет диапазон до 255, поэтому вы можете выполнить цикл 254 раза (если вы попытаетесь сделать 255, он переполнится, и цикл никогда не закончится, но вам не нужно останавливаться на 128).

В-четвертых, вы не делаете нигде рядом с итерациями в профиль. Итерирование плотной петли 128 или даже 254 раз бессмысленно. То, что вы должны делать, - это положить цикл byte/short/int внутри другого цикла, который повторяет гораздо большее количество раз, скажем, 10 миллионов, и проверяет результаты этого.

Наконец, использование DateTime.Now в рамках расчетов приведет к некоторому временному "шуму" при профилировании. Рекомендуется (и проще) использовать Stopwatch.

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


Вот то, что я считаю более точной тестовой программой:

class Program
{
    const int TestIterations = 5000000;

    static void Main(string[] args)
    {
        RunTest("Byte Loop", TestByteLoop, TestIterations);
        RunTest("Short Loop", TestShortLoop, TestIterations);
        RunTest("Int Loop", TestIntLoop, TestIterations);
        Console.ReadLine();
    }

    static void RunTest(string testName, Action action, int iterations)
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        for (int i = 0; i < iterations; i++)
        {
            action();
        }
        sw.Stop();
        Console.WriteLine("{0}: Elapsed Time = {1}", testName, sw.Elapsed);
    }

    static void TestByteLoop()
    {
        int x = 0;
        for (byte b = 0; b < 255; b++)
            ++x;
    }

    static void TestShortLoop()
    {
        int x = 0;
        for (short s = 0; s < 255; s++)
            ++x;
    }

    static void TestIntLoop()
    {
        int x = 0;
        for (int i = 0; i < 255; i++)
            ++x;
    }
}

Это запускает каждый цикл внутри гораздо большего цикла (5 миллионов итераций) и выполняет очень простую операцию внутри цикла (увеличивает значение переменной). Для меня были следующие результаты:

Байт-цикл: Истекшее время = 00: 00: 03.8949910
Короткая петля: Истекшее время = 00: 00: 03.9098782
Int Loop: Истекшее время = 00: 00: 03.2986990

Таким образом, нет заметной разницы.

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

Ответ 2

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

Дополнительно:

  • Использование DateTime.Now - плохой способ измерения времени. Вместо этого используйте System.Diagnostics.Stopwatch
  • Как только вы избавились от вызова Console.WriteLine, цикл из 127 итераций будет слишком коротким для измерения. Вам нужно запустить цикл много раз, чтобы получить разумное измерение.

Здесь мой бенчмарк:

using System;
using System.Diagnostics;

public static class Test
{    
    const int Iterations = 100000;

    static void Main(string[] args)
    {
        Measure(ByteLoop);
        Measure(ShortLoop);
        Measure(IntLoop);
        Measure(BackToBack);
        Measure(DelegateOverhead);
    }

    static void Measure(Action action)
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        Stopwatch sw = Stopwatch.StartNew();
        for (int i = 0; i < Iterations; i++)
        {
            action();
        }
        sw.Stop();
        Console.WriteLine("{0}: {1}ms", action.Method.Name,
                          sw.ElapsedMilliseconds);
    }

    static void ByteLoop()
    {
        for (byte index = 0; index < 127; index++)
        {
            index.ToString();
        }
    }

    static void ShortLoop()
    {
        for (short index = 0; index < 127; index++)
        {
            index.ToString();
        }
    }

    static void IntLoop()
    {
        for (int index = 0; index < 127; index++)
        {
            index.ToString();
        }
    }

    static void BackToBack()
    {
        for (byte index = 0; index < 127; index++)
        {
            index.ToString();
        }
        for (short index = 0; index < 127; index++)
        {
            index.ToString();
        }
        for (int index = 0; index < 127; index++)
        {
            index.ToString();
        }
    }

    static void DelegateOverhead()
    {
        // Nothing. Let see how much
        // overhead there is just for calling
        // this repeatedly...
    }
}

И результаты:

ByteLoop: 6585ms
ShortLoop: 6342ms
IntLoop: 6404ms
BackToBack: 19757ms
DelegateOverhead: 1ms

(Это на нетбуке - отрегулируйте количество итераций, пока не получите что-то разумное:)

Это похоже на то, что он делает практически не существенным, какой тип вы используете.

Ответ 3

Просто из любопытства я модифицировал программу Aaronaught и скомпилировал ее в режимах x86 и x64. Strange, Int работает быстрее в x64:

x86

Байт-цикл: Истекшее время = 00: 00: 00.8636454
Короткая петля: Истекшее время = 00: 00: 00.8795518
UShort Loop: Истекшее время = 00: 00: 00.8630357
Int Loop: Истекшее время = 00: 00: 00.5184154
UInt Loop: Истекшее время = 00: 00: 00.4950156
Длительная петля: Истекшее время = 00: 00: 01.2941183
ULong Loop: Истекшее время = 00: 00: 01.3023409

64

Байт-цикл: Истекшее время = 00: 00: 01.0646588
Короткая петля: Истекшее время = 00: 00: 01.0719330
UShort Loop: Истекшее время = 00: 00: 01.0711545
Int Loop: Истекшее время = 00: 00: 00.2462848
UInt Loop: Истекшее время = 00: 00: 00.4708777
Длинные петли: Истекшее время = 00: 00: 00.5242272
ULong Loop: Истекшее время = 00: 00: 00.5144035

Ответ 4

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

Выходы из тестового жгута Ааронотса

Short Loop: Elapsed Time = 00:00:00.8299340
Byte Loop: Elapsed Time = 00:00:00.8398556
Int Loop: Elapsed Time = 00:00:00.3217386
Long Loop: Elapsed Time = 00:00:00.7816368

ints намного быстрее

Вывод от Jon's

ByteLoop: 1126ms
ShortLoop: 1115ms
IntLoop: 1096ms
BackToBack: 3283ms
DelegateOverhead: 0ms

ничего в нем

У Джона есть большая фиксированная константа вызова tostring в результатах, которые могут скрывать возможные преимущества, которые могут возникнуть, если работа, выполненная в цикле, меньше. Aaronaught использует 32-битную ОС, которая, по-видимому, не пользуется использованием Ints, а также используемой мной установкой x64.

Оборудование/Программное обеспечение Результаты были собраны на Core i7 975 на частоте 3,33 ГГц с отключением турбонаддува, а сродство керна уменьшило влияние других задач. Настройки производительности все установлены на максимальный, а вирусный сканер/ненужные фоновые задачи приостановлены. Windows 7 x64 с 11 ГБ свободного бара и очень мало активности ввода-вывода. Запуск в версии release, встроенный в vs 2008 без отладки или профайлера.

Повторяемость Первоначально повторялось 10-кратное изменение порядка выполнения для каждого теста. Изменение было незначительным, поэтому я только опубликовал свой первый результат. При максимальной загрузке процессора соотношение времени выполнения оставалось неизменным. Повторение выполняется на нескольких blade-серверах x64 xp xeon дает примерно одинаковые результаты после учета выработки ЦП и Ghz

Профилирование Профилировщик Redgate/Jetbrains/Slimtune/CLR и мой собственный профилировщик указывают, что результаты верны.

Отладка сборки Использование настроек отладки в VS дает согласованные результаты, такие как Aaronaught's.

Ответ 5

Профилирование .Net-кода очень сложно, поскольку среда выполнения, с которой выполняется скомпилированный байт-код, может выполнять оптимизацию во время выполнения по байтовому коду. В вашем втором примере компилятор JIT, вероятно, заметил повторяющийся код и создал более оптимизированную версию. Но, без какого-либо подробного описания того, как работает система времени выполнения, невозможно знать, что произойдет с вашим кодом. И было бы глупо пытаться угадывать, основываясь на экспериментах, поскольку Microsoft полностью в пределах своих прав переделывать JIT-движок в любое время, если они не нарушают никаких функциональных возможностей.

Ответ 6

Консольная запись имеет нулевое значение для фактической производительности данных. Это больше связано с взаимодействием с вызовами библиотеки консоли. Предложите вам сделать что-то интересное внутри этих циклов, которые не зависят от размера данных.

Предложения: бит сдвиги, умножения, манипуляции с массивами, добавление, многие другие...