2016-08-17 13 views
2

Я новичок в программировании SSE, поэтому я надеюсь, что кто-то там может мне помочь. Недавно я реализовал функцию, использующую встроенные функции GCC SSE для вычисления суммы массива из 32-разрядных целых чисел. Код для моей реализации приведен ниже.Более медленная производительность SSE при больших размерах массивов

int ssum(const int *d, unsigned int len) 
{ 
    static const unsigned int BLOCKSIZE=4; 
    unsigned int i,remainder; 
    int output; 
    __m128i xmm0, accumulator; 
    __m128i* src; 

    remainder = len%BLOCKSIZE; 
    src = (__m128i*)d; 
    accumulator = _mm_loadu_si128(src); 

    output = 0; 
    for(i=BLOCKSIZE;i<len-remainder;i+=BLOCKSIZE){ 
    xmm0 = _mm_loadu_si128(++src); 
    accumulator = _mm_add_epi32(accumulator,xmm0); 
    } 

    accumulator = _mm_add_epi32(accumulator, _mm_srli_si128(accumulator, 8)); 
    accumulator = _mm_add_epi32(accumulator, _mm_srli_si128(accumulator, 4)); 
    output = _mm_cvtsi128_si32(accumulator); 


    for(i=len-remainder;i<len;i++){ 
    output += d[i]; 
    } 
    return output; 
} 

Как вы можете видеть, это довольно прямо вперед, реализация которой я просуммировать массив 4 в то время, используя расширенные регистры XMM, а затем очистить в конце сложения остальных элементов.

Затем я сравнил производительность этой реализации SIMD с простым прохождением цикла. В результате этого эксперимента можно найти здесь:

SIMD vs. for-loop

Как вы можете видеть, по сравнению с для цикла, эта реализация действительно показывает около ~ 60% прирост скорости в течение входных размеров (то есть длина из массив) до примерно 5 М элементов. Однако при больших значениях входного размера производительность по отношению к циклу for берет резкое погружение и производит только около 20% ускорения.

Я затрудняюсь объяснить это резкое снижение производительности. Я более или менее чередуюсь линейно по памяти, поэтому влияние промахов кэша и ошибок страниц должно быть примерно одинаковым для обеих реализаций. Что мне здесь не хватает? Есть ли способ сгладить эту кривую? Любые мысли были бы весьма признательны.

+1

Какой процессор вы используете? –

+2

Во-первых, вы проверили, будет ли gcc autovectorizes скалярным кодом? Во-вторых, вы, вероятно, будете ограничены пропускной способностью. – EOF

+0

Как @EOF говорит, вы почти ничего не делаете в своем цикле (одна арифметическая инструкция SIMD), поэтому вы, скорее всего, будете ограничены пропускной способностью памяти, если у вас большие массивы. –

ответ

4

Для большого ввода данные находятся за пределами кеша, а код ограничен памятью.
Для небольшого ввода данные находятся в кеше (например, кеш L1/L2/L3), а код - ограниченный.
Я предполагаю, что вы не пытались сбросить кеш, прежде чем измерение производительности.

Кэш-память находится внутри ЦП, а пропускная способность между кэш-памятью и блоками ALU (или SSE) очень высока (высокая пропускная способность - меньше времени передачи данных).
Размер кеша наивысшего уровня (i.e L3) составляет от 4 МБ до 8 МБ (в зависимости от модели вашего процессора).
Максимальное количество данных должно быть расположено на DDR SDRAM, ведь это внешнее ОЗУ (вне ЦП).
Процессор подключен к DDR SDRAM с шиной памяти, с гораздо меньшей пропускной способностью, чем кэш-память.

Пример:
Предположим, что ваш внешний тип ОЗУ - Dual Channel DDR3 SDRAM 1600. Максимальная теоретическая пропускная способность между внешней оперативной памятью и процессором составляет около 25 ГБ/сек.

Чтение 100 Мбайт данных (при 25 ГБ/с) из ОЗУ в CPU занимает около 100e6/25e9 = 4 мс.
По моему опыту, используемая полоса пропускания составляет около половины теоретической пропускной способности, поэтому время чтения составляет около 8 мсек.

Время вычислений короче:
Предположим, что каждая итерация вашей петли занимает около 2 часов процессора (всего лишь пример).
Каждый процесс итерации 16 байтов данных.
Всего процессорных часов для обработки 100 МБ занимает около (100e6/16) * 2 = 12500000 clks.
Предположим, что частота процессора составляет 3GHz.
Общее время обработки SSE составляет около 12500000/3e9 = 4.2 мсек.

Как вы можете видеть, чтение данных из внешнего ОЗУ занимает в два раза больше, чем время вычисления SSE.

Поскольку передача данных и вычисления происходят параллельно, общее время составляет максимум 4,2 мс и 8 мс (т.е. 8 мсек).

Предполагается, что цикл без использования SSE занимает в два раза больше времени вычисления, поэтому без использования SSE время вычисления составляет около 8,4 мсек.

В приведенном выше примере общее улучшение использования SSE составляет около 0,4 мсек.

Примечание: Выбранные номера предназначены, например, для целей.


Ориентиры:
Я сделал несколько тестов на моей системе.
Я использую Windows 10 и Visual Studio 2010.
Тест Benchmark: суммирование 100 Мбайт данных (суммирование 25 * 1024^2 32 бита целых чисел).

CPU

  • Intel Core i5 3550 (Ivy Bridge).
  • Базовая частота процессора 3,3 ГГц.
  • Фактическая частота ядра во время теста: 3,6 ГГц (включен режим Turbo).
  • L1 данные кеш размер: 32KBytes.
  • Размер кэш-памяти L2: 256 бит (размер одного кеша L2).
  • L3 кеш размер: 6MBytes.

Память:

  • 8GB DDR3 Двухканальный.
  • Частота ОЗУ: 666 МГц (эквивалентно 1333 МГц без DDR).
  • Теоретическая максимальная пропускная способность памяти: (128 * 1333/8)/1024 = 20,8 ГБ/сек.

  1. Сумма 100MB, как большой кусок с SSE (данные во внешнем ОЗУ).
    Время обработки: 6.22msec
  2. Сумма 1KB 100 раз с SSE (данные внутри кеша).
    Время обработки: 3,86 мс
  3. Сумма 100 МБ как большая часть без SSE (данные во внешней ОЗУ).
    Время обработки: 8.1msec
  4. Сумма 1KB 100 раз без SSE (данные внутри кеша).
    Время обработки: 4.73msec

Использовано пропускная способность памяти: 100/6,22 = 16GB/сек(разделив объем данных по времени).
Средние часы на итерацию с SSE (данные в кеше): (3.6e9 * 3.86e-3)/(25/4 * 1024^2) = 2.1 clks/iteration(разделение общих тактовых импульсов процессора по количеству итераций).

+0

Это такой подробный ответ. Спасибо! Учитывая полученные результаты, вы думаете, может быть, есть какое-то преимущество в буферизации программного обеспечения? В основном копирование памяти из DRAM 32KB за раз (скажем) во второй управляемый программой буфер и выполнение там вычислений? Я бы предположил, что это приведет к загрузке шрифта из производительности в основной памяти и не приведет к провалу кеша при 64 байтах (длина строки кэша). Я не специалист по компьютерной архитектуре любым способом, поэтому, пожалуйста, не стесняйтесь говорить мне, если это полный сумасшедший разговор. – voidbip

+0

Я использовал это, когда я запрограммировал DSP (инициировал передачу DMA из внешней RAM в внутреннее ОЗУ DSP). Современная архитектура x86 использует автоматический механизм предварительной выборки, который обнаруживает шаблон доступа к памяти (то есть присоединяющиеся адреса) и считывает данные в кеш, прежде чем программа его использует. Недостатки кэша здесь не проблема, производительность ограничена пропускной способностью памяти (вы просто не можете быстрее читать данные из DDR SDRAM). – Rotem

+0

@voidbip: Это хорошая идея, но на самом деле ужасно. Это означает, что ни одно из ваших вычислений не может совпадать с пропущенными вами кешами, когда вы впервые касаетесь холодной памяти. Кэш L1/L2/L3 может кэшировать любую часть основной памяти, а дополнительное копирование просто увеличивает размер вашего кеша. Отскок к маленькому буферу, который остается горячим в L1, полезен для очень редких случаев, таких как [сохранение загрузки NT из видеопамяти отдельно от хранилищ NT в обычную RAM] (https://software.intel.com/en-us/articles/copying -accelerated-видео-декодирования-Frame-буфера). До тех пор, пока вы не поймете эту статью, не делайте этого. –

 Смежные вопросы

  • Нет связанных вопросов^_^