Почему массивы UInt16, кажется, добавляют быстрее, чем массивы int? - PullRequest
8 голосов
/ 13 апреля 2010

Кажется, что 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 ]

6 голосов
/ 13 апреля 2010

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

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

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

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

2 голосов
/ 13 апреля 2010

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

1 голос
/ 13 апреля 2010

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

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

Пара факторов

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

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

1 голос
/ 13 апреля 2010

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

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

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

...