(фон: Почему я должен использовать интервал вместо байта или короткий в C#),
Для удовлетворения моего собственного любопытства о за и против использования "соответствующего размера" целое число по сравнению с "оптимизированным" целым числом, я написал следующий код, который укрепил то, что я ранее сохранялся о международной производительности в .NET (и который объяснен в ссылке выше), который является, что это оптимизировано для международной производительности, а не короткое или байт.
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 из-за диапазона типа значения байта. Также это - действие любопытства не производственная микрооптимизация кода.
Во-первых, не .NET оптимизирован для производительности int
, а машина оптимизирована, потому что 32 бита - это собственный размер слова (если вы не используете x64, в этом случае это long
или 64 бита).
Во-вторых, вы пишете в консоль внутри каждого цикла - это будет намного дороже, чем увеличение и тестирование счетчика цикла, так что вы здесь не измеряете ничего реалистичного.
В-третьих, байт
имеет диапазон до 255, поэтому вы можете выполнить цикл 254 раза (если вы попытаетесь сделать 255, он переполнится, и цикл никогда не закончится, но вам не нужно останавливаться на 128).
В-четвертых, вы нигде не делаете около достаточного количества итераций для профилирования. Повторять жесткий цикл 128 или даже 254 раз бессмысленно. Что вам нужно сделать, так это поместить цикл byte
/ short
/ int
в другой цикл, который повторяется гораздо большее количество раз, скажем 10 миллионов, и проверить результаты этого.
Наконец, использование DateTime.Now
в вычислениях приведет к некоторому временному «шуму» при профилировании. Вместо этого рекомендуется (и это проще) использовать класс Секундомер .
В итоге, для этого требуется много изменений, прежде чем он станет действительным тестом производительности.
Вот то, что я считаю более точной тестовой программой:
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 миллионов итераций) и выполняет очень простую операцию внутри цикла (увеличивает переменную на единицу). Результаты для меня были такими:
Byte Loop: Elapsed Time = 00: 00: 03.8949910
Short Loop: Elapsed Time = 00: 00: 03.9098782
Int Loop: Истекшее время = 00: 00: 03.2986990
Итак, заметной разницы нет.
Также убедитесь, что вы профилируете в режиме выпуска, многие люди забывают и тестируют в режиме отладки, который будет значительно менее точным.
Профилирование .Net-кода очень сложно, потому что среда выполнения, в которой выполняется скомпилированный байт-код, может выполнять оптимизацию байтового кода во время выполнения. Во втором примере JIT-компилятор, вероятно, обнаружил повторяющийся код и создал более оптимизированную версию. Но без какого-либо действительно подробного описания того, как работает система времени выполнения, невозможно знать, что произойдет с вашим кодом. И было бы глупо пытаться догадываться на основе экспериментов, поскольку Microsoft полностью вправе в любое время изменить дизайн JIT-движка, при условии, что они не нарушат никаких функциональных возможностей.
Я опробовал две указанные выше программы, так как они выглядели так, как будто они давали разные и, возможно, противоречивые результаты на моей машине разработчика.
Вывод из тестовой среды Ааронотса
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
int намного быстрее
Вывод от Джона
ByteLoop: 1126ms
ShortLoop: 1115ms
IntLoop: 1096ms
BackToBack: 3283ms
DelegateOverhead: 0ms
ничего в нем
Джон имеет большую фиксированную константу вызова tostring в результатах, которая может скрывать возможные преимущества это могло бы произойти, если бы работа, выполняемая в цикле, была меньше. Aaronaught использует 32-битную ОС, которая, похоже, не получит такой же выгоды от использования int, как установка x64, которую я использую.
Аппаратное / программное обеспечение Результаты были получены на процессоре Core i7 975 с тактовой частотой 3,33 ГГц с отключенным турбо-режимом и привязкой к ядрам, настроенной для уменьшения влияния других задач. Все настройки производительности установлены на максимум, а антивирусные программы / ненужные фоновые задачи приостановлены. Windows 7 x64 ultimate с 11 ГБ свободной оперативной памяти и очень низкой активностью ввода-вывода. Запуск в конфигурации выпуска, встроенной в vs 2008, без подключенного отладчика или профилировщика.
Повторяемость Первоначально повторяется 10 раз, изменяя порядок выполнения для каждого теста. Вариации были незначительны, поэтому я опубликовал только свой первый результат.При максимальной загрузке процессора соотношение времени выполнения оставалось неизменным. Повторные прогоны на нескольких блейд-серверах x64 xp xeon дают примерно одинаковые результаты с учетом поколения ЦП и ГГц
Профилирование Профилировщик Redgate / Jetbrains / Slimtune / CLR и мой собственный профилировщик все показывают, что результаты верный.
Сборка отладки Использование настроек отладки в VS дает стабильные результаты, такие как у Aaronaught.
Консольная запись не имеет никакого отношения к фактической производительности данных. Это больше связано с взаимодействием с вызовами консольной библиотеки. Предложите вам сделать что-нибудь интересное внутри этих циклов, не зависящее от размера данных.
Предложения: битовые сдвиги, умножение, манипуляции с массивами, сложение и многое другое ...
Большая часть этого времени, вероятно, тратится на запись в консоль. Попробуйте сделать что-нибудь кроме этого в цикле ...
Дополнительно:
DateTime.Now
- плохой способ измерения времени. Используйте System.Diagnostics.Stopwatch
вместо .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's see how much
// overhead there is just for calling
// this repeatedly...
}
}
И результаты:
ByteLoop: 6585ms
ShortLoop: 6342ms
IntLoop: 6404ms
BackToBack: 19757ms
DelegateOverhead: 1ms
(Это на нетбуке - отрегулируйте количество итераций, пока не получите что-то разумное :)
Кажется, это показывает, что в основном нет существенных различий. тип, который вы используете.