Кажется, что C# быстрее при добавлении двух массивов UInt16[]
чем он при добавлении двух массивов int[]
. Это не имеет никакого смысла мне, так как я предположил бы, что массивы будут выровнены словом, и таким образом int[]
потребовал бы меньшего количества работы от ЦП, нет?
Я выполнил тестовый код ниже и получил следующие результаты:
Int for 1000 took 9896625613 tick (4227 msec)
UInt16 for 1000 took 6297688551 tick (2689 msec)
Тестовый код делает следующее:
a
и b
, однажды.a
и b
, пообъектно. Это сделано 1000 раз.Это сделано для int[] a, b
и для UInt16 a,b
. И каждый раз я выполняю код, тесты для UInt16
массивы занимают на 30%-50% меньше времени, чем int
массивы. Можно ли объяснить это мне?
Вот код, если Вы хотите попробовать если за себя:
public static UInt16[] GenerateRandomDataUInt16(int length)
{
UInt16[] noise = new UInt16[length];
Random random = new Random((int)DateTime.Now.Ticks);
for (int i = 0; i < length; ++i)
{
noise[i] = (UInt16)random.Next();
}
return noise;
}
public static int[] GenerateRandomDataInt(int length)
{
int[] noise = new int[length];
Random random = new Random((int)DateTime.Now.Ticks);
for (int i = 0; i < length; ++i)
{
noise[i] = (int)random.Next();
}
return noise;
}
public static int[] AddInt(int[] a, int[] b)
{
int len = a.Length;
int[] result = new int[len];
for (int i = 0; i < len; ++i)
{
result[i] = (int)(a[i] + b[i]);
}
return result;
}
public static UInt16[] AddUInt16(UInt16[] a, UInt16[] b)
{
int len = a.Length;
UInt16[] result = new UInt16[len];
for (int i = 0; i < len; ++i)
{
result[i] = (ushort)(a[i] + b[i]);
}
return result;
}
public static void Main()
{
int count = 1000;
int len = 128 * 6000;
int[] aInt = GenerateRandomDataInt(len);
int[] bInt = GenerateRandomDataInt(len);
Stopwatch s = new Stopwatch();
s.Start();
for (int i=0; i<count; ++i)
{
int[] resultInt = AddInt(aInt, bInt);
}
s.Stop();
Console.WriteLine("Int for " + count
+ " took " + s.ElapsedTicks + " tick ("
+ s.ElapsedMilliseconds + " msec)");
UInt16[] aUInt16 = GenerateRandomDataUInt16(len);
UInt16[] bUInt16 = GenerateRandomDataUInt16(len);
s = new Stopwatch();
s.Start();
for (int i=0; i<count; ++i)
{
UInt16[] resultUInt16 = AddUInt16(aUInt16, bUInt16);
}
s.Stop();
Console.WriteLine("UInt16 for " + count
+ " took " + s.ElapsedTicks + " tick ("
+ s.ElapsedMilliseconds + " msec)");
}
Вы видите дырявую абстракцию. UInt16 занимает половину памяти, чем int (16 против 32 бит).
Это означает, что область памяти, занятая массивом int16, занимает половину площади, чем int32. Таким образом, большая часть этой области может уместиться в кеш-памяти процессора и, таким образом, доступна очень быстро.
Вы можете попробовать этот код на процессоре, у которого больше кэш-памяти, и разница, вероятно, будет меньше.
Также попробуйте с гораздо большими массивами.
Массивы выровнены по словам, но нет причин, по которым записи в массиве должны быть выровнены по словам.
Просто SWAG: меньшее использование памяти массивами UInt16 улучшило характеристики памяти (GC, кеш, кто знает что еще). Поскольку кажется, что выделений не так много, я предполагаю, что кеш является основным фактором.
Кроме того, вы должны позаботиться о том, чтобы тестирование производительности могло оказаться непростым делом - похоже, ваше время, вероятно, включает некоторую JIT-компиляцию, которая может искажать результаты. Вы можете попробовать изменить порядок, в котором вы тестируете массив int
с массивом UInt16
, и посмотреть, совпадают ли тайминги или нет.
У Джона Скита была (или была) простая структура тестирования производительности, которую он закодировал еще тогда, когда пытался учесть эти эффекты. Я не знаю, доступен ли он (или вообще применим); может он прокомментирует.
Я не эксперт в .NET, но я бы проверил две вещи:
int
) занимает больше времени, чем массив из N ushort
элементов. Это можно проверить, используя различные размеры массивов и стиль кодирования - см. мой комментарий к вопросу). Числа из ваших тестов соответствуют этой теории :). ushort
переменных может быть реализовано как сложение двух int
с результатом типа int
-- без проверки переполнения. И я предполагаю, что обработка в коде любого вида исключения (включая исключение переполнения) является трудоемкой задачей. Это можно проверить в документации .NET. Пара факторов
1) Вы также синхронизация генерации результирующего массива ... поэтому было бы интересно посмотреть, сколько времени потребовалось только на добавление, а не на создание результирующего массива, который передается обратно
2) Было бы интересно посмотреть, какой IL генерируется . Поскольку ваш код ОЧЕНЬ прост (итерация и добавление), компилятор может оптимизировать его, возможно, вставляя несколько uint16 в больший регистр и выполняя несколько добавлений для каждой инструкции