Почему массивы UInt16 добавляют быстрее, чем массивы int?

кажется, что c# быстрее при добавлении двух массивов UInt16[] чем при добавлении двух массивов int[]. Это не имеет смысла для меня, так как я бы предположил, что массивы будут выровнены по словам, и, следовательно,int[] потребует меньше работы от процессора, нет?

я запустил тест-код ниже и получил следующие результаты:

Int    for 1000 took 9896625613 tick (4227 msec)
UInt16 for 1000 took 6297688551 tick (2689 msec)

тестовый код делает следующее:

  1. создает два массива с именем a и b, однажды.
  2. заполняет их случайными данными, после.
  3. запуск секундомера.
  4. добавляет a и b, пункт за пунктом. Это делается 1000 раз.
  5. останавливает секундомер.
  6. сообщает, сколько времени это заняло.

это сделано для 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)");


}

5 ответов


Что происходит, так это то, что вы видите дырявую абстракцию. UInt16 занимает половину памяти, которую делает int (16 против 32 бит).

Это означает, что область памяти, занимаемая массивом int16, занимает половину области, занимаемой int32. Таким образом, большая часть этой области может поместиться в кэш процессора и, таким образом, получить доступ очень быстро.

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

также попробуйте с гораздо большими массивами.


массивы выровнены по словам, но нет причин, по которым записи в массиве должны быть выровнены по словам.


пару факторов

1) вы также синхронизируете генерацию результирующего массива..поэтому было бы интересно посмотреть, сколько времени потребовалось только для добавления vs создания массива результатов, который возвращается

2) было бы интересно посмотреть, что генерируется IL. Поскольку ваш код очень прост (итерация и добавление), компилятор может оптимизировать это, возможно, заполняя несколько uint16 в большем регистре и делая несколько дополнений за инструкция


просто SWAG: меньшее использование памяти массивов UInt16 улучшило характеристики памяти (GC, кэш, кто знает, что еще). Поскольку, похоже, не слишком много распределений, я бы предположил, что кэш является основным фактором.

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

Джон Скит имеет (или имел) простую базовую структуру, которую он закодировал, когда пытался учесть эти эффекты. Я не знаю, доступен ли он (или даже применим); возможно, он прокомментирует.


Я не эксперт в .NET, но я бы проверил две вещи:

  1. передает больший массив (N элементов типа int) занимает больше времени, чем массив из N ushort элементы. Это можно проверить, используя различные размеры массивов и стиль кодирования - см. Мой комментарий к вопросу). Числа из ваших тестов соответствуют этой теории :).
  2. добавлять два ushort переменные могут быть реализованы как добавление двух int с результатом типа int -- без проверка переполняя. И я предполагаю, что обращение в коде какой-либо исключение (включая исключение переполнения) составляет времени задач. Это можно проверить в документации .NET.