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
Консольная запись имеет нулевое значение для фактической производительности данных. Это больше связано с взаимодействием с вызовами библиотеки консоли. Предложите вам сделать что-то интересное внутри этих циклов, которые не зависят от размера данных.
Предложения: бит сдвиги, умножения, манипуляции с массивами, добавление, многие другие...