2012-05-15 2 views
28

TL; DR: Почему данные умножения/литья в size_t медленны и почему это зависит от платформы?Производительность от size_t до double

У меня возникают проблемы с производительностью, которые я не совсем понимаю. Контекст - это захват кадра камеры, где изображение 128x128 uint16_t считывается и обрабатывается с частотой 100 Гц.

В постобработке я генерирую гистограмму frame->histo, которая составляет uint32_t, и имеет thismaxval = 2^16 элементов, в основном я подсчитываю все значения интенсивности. Используя эту гистограмму рассчитать сумму и сумму квадратов:

double sum=0, sumsquared=0; 
size_t thismaxval = 1 << 16; 

for(size_t i = 0; i < thismaxval; i++) { 
    sum += (double)i * frame->histo[i]; 
    sumsquared += (double)(i * i) * frame->histo[i]; 
} 

Профилирование кода с профилем я получил следующее (образцы, процент, код):

58228 32.1263 : sum += (double)i * frame->histo[i]; 
116760 64.4204 : sumsquared += (double)(i * i) * frame->histo[i]; 

или первая строка занимает 32 % от процессорного времени, вторая - 64%.

Я сделал некоторый бенчмаркинг, и это похоже на тип данных/литье, что проблематично. Когда я меняю код на

uint_fast64_t isum=0, isumsquared=0; 

for(uint_fast32_t i = 0; i < thismaxval; i++) { 
    isum += i * frame->histo[i]; 
    isumsquared += (i * i) * frame->histo[i]; 
} 

он работает ~ 10 раз быстрее. Однако эта производительность также зависит от платформы. На рабочей станции процессор Core i7 950 @ 3.07GHz код в 10 раз быстрее. На моем Macbook8,1, который имеет Intel Core i7 Sandy Bridge 2.7 ГГц (2620M), код только в 2 раза быстрее.

Теперь я задаюсь:

  1. Почему исходный код так медленно и легко разогнал?
  2. Почему это зависит от платформы?

Update:

Я составил приведенный выше код с

g++ -O3 -Wall cast_test.cc -o cast_test 

UPDATE2:

Я побежал оптимизированные коды через профайлер (Instruments на Mac, как Shark) и нашел две вещи:

1) В некоторых случаях сам цикл занимает значительное количество времени. thismaxval имеет тип size_t.

  1. for(size_t i = 0; i < thismaxval; i++) занимает 17% от общего времени выполнения моего
  2. for(uint_fast32_t i = 0; i < thismaxval; i++) занимает 3,5%
  3. for(int i = 0; i < thismaxval; i++) не появляется в профилировщике, я предполагаю, что это меньше, чем 0.1%

2) Типы данных и литья независимо от того, следующим образом:

  1. sumsquared += (double)(i * i) * histo[i]; 15% (с size_t i)
  2. sumsquared += (double)(i * i) * histo[i]; 36% (с uint_fast32_t i)
  3. isumsquared += (i * i) * histo[i]; 13% (с uint_fast32_t i , uint_fast64_t isumsquared)
  4. isumsquared += (i * i) * histo[i]; 11% (с int i, uint_fast64_t isumsquared)

Удивительно, но int быстрее, чем uint_fast32_t?

Update4:

я провел еще несколько тестов с различными типами данных и различных компиляторов, на одной машине. Результаты приведены ниже.

Для testd 0 - 2 соответствующий код

for(loop_t i = 0; i < thismaxval; i++) 
     sumsquared += (double)(i * i) * histo[i]; 

с sumsquared двойным, и loop_tsize_t, uint_fast32_t и int для тестов 0, 1 и 2.

Для испытаний 3-- 5 код

for(loop_t i = 0; i < thismaxval; i++) 
     isumsquared += (i * i) * histo[i]; 

с isumsquared типа uint_fast64_t и loop_t снова size_t, uint_fast32_t и int для испытаний 3, 4 и 5.

Составители я Используемые GCC 4.2.1, 4.4.7 GCC, GCC 4.6.3 и GCC 4.7.0. Тайминги находятся в процентах от общего времени процессора, поэтому они показывают относительную производительность, а не абсолютную (хотя время выполнения было довольно постоянным в 21 секунду). Время cpu для обеих двух строк, потому что я не совсем уверен, правильно ли профайлер разделяет две строки кода.

 
gcc: 4.2.1 4.4.7 4.6.3 4.7.0 
---------------------------------- 
test 0: 21.85 25.15 22.05 21.85 
test 1: 21.9 25.05 22  22 
test 2: 26.35 25.1 21.95 19.2 
test 3: 7.15 8.35 18.55 19.95 
test 4: 11.1 8.45 7.35 7.1 
test 5: 7.1 7.8 6.9 7.05 

или:

casting performance

Исходя из этого, кажется, что литье дорого, независимо от того, какого типа целое я использую.

Кроме того, кажется, что gcc 4.6 и 4.7 не могут правильно оптимизировать петлю 3 (size_t и uint_fast64_t).

+0

Вы также можете попробовать его с помощью 'uint_fast32_t'? Дикая догадка заключается в том, что она быстрее из-за того, что второй тип данных имеет ту же длину бит, что и машинные команды (64-разрядные). Угадаю, что у вас есть 64-битная машина. Я бы ожидал, что fast32 также будет медленнее. [edit] можете ли вы также проверить размер как 'uint_fast32_t', так и' uint_fast64_t'? Я предполагаю, что 32 на самом деле 64 бит. – Yuri

+0

Вы имеете в виду 'uint_fast32_t isum'? Я мог бы попробовать, хотя я думаю, что это может переполняться, поэтому я использовал uint_fast64_t. – Tim

+3

Ну, для 1 .: Причина почему-то диктует, что литье ints должно плавать, а операции float должны быть медленнее, чем делать операции int непосредственно (хотя int-to-float не должно быть таким же злым, как float-to-int), еще больше поэтому с не оптимальным стеком x87. Вы скомпилируете его с поддержкой SSE? –

ответ

4

Для ваших оригинальных вопросов:

  1. Код медленно, потому что она включает в себя преобразование из целого числа типов с плавающей точкой данных. Вот почему он легко ускоряется, когда вы используете также целочисленный тип данных для суммарных переменных, поскольку он не требует больше float-conversion.
  2. Разница заключается в нескольких факторах .Например, это зависит от того, насколько эффективно платформа может выполнить преобразование int-> float. Кроме того, это преобразование может также испортить внутреннюю оптимизацию процессора в программе . Механизмы потока и прогнозирования, кэши, ... а также внутренние функции распараллеливания параллельных вычислений могут иметь огромное влияние на вычисления .

Для дополнительных вопросов:

  • "Удивительно ИНТ быстрее, чем uint_fast32_t"? Что такое sizeof (size_t) и sizeof (int) на вашей платформе? Можно предположить, что оба являются , вероятно, 64 бит, и поэтому приведение к 32 бит не только может дать вам ошибки расчета , но также включает штраф в размере .

В целом старайтесь избегать видимых и скрытых слепок как можно лучше, если они не нужны. Например, попробуйте выяснить, какой реальный тип данных скрыт за «size_t» в вашей среде (gcc) и использовать его для переменной цикла. В вашем примере квадрат uint's не может быть типом данных с плавающей точкой, поэтому нет смысла использовать double здесь. Придерживайтесь целых типов для достижения максимальной производительности.

+0

Спасибо. Я знал, что отливки не идеальны, но я не знал, что это так дорого. Что касается вашего второго пункта: машина имеет 64 бит, но 'uint_fast32_t' должен быть ** не менее ** 32 бит, если я правильно ее понимаю, так что если 64bit не должен использовать это вместо этого? Я не понимаю, почему это объясняет, что 'int' быстрее, чем' uint_fast32_t'. Кроме того, я не понимаю, почему производительность 'for'-loop сильно отличается от разных целых типов. – Tim

+1

Ну, поскольку 'uint_fast32_t' должен быть самым быстрым целым типом с не менее чем 32 битами, реализация должна быть достаточно умной, чтобы использовать 64-битный тип, если это происходит быстрее в 64-битной системе. Но в остальном хороший ответ, конечно. –

+0

Я провела еще несколько тестов, и суть в том, что литье просто дорого. Я был удивлен, что это было дорого. – Tim

1

На x86, преобразование uint64_t с плавающей точкой происходит медленнее, потому что есть только инструкции для преобразования int64_t, int32_t и int16_t. int16_t и в 32-битном режиме int64_t может быть преобразован только с помощью инструкций x87, а не SSE.

При преобразовании uint64_t с плавающей точкой, GCC 4.2.1 сначала преобразует значение, как если бы это было int64_t, а затем добавляет 2 , если он был отрицательным, чтобы компенсировать. (При использовании x87, в Windows и * BSD, или если вы изменили контроль точности, остерегайтесь того, чтобы преобразование игнорировало контроль точности, но дополнение уважает его.)

uint32_t сначала расширен до int64_t.

При преобразовании 64-разрядных целых чисел в 32-разрядный режим на процессорах с определенными 64-битными возможностями проблема пересылки между хранилищем и загрузкой может вызвать киоски. 64-разрядное целое число записывается как два 32-битных значения и считывается как одно 64-битное значение. Это может быть очень плохо, если преобразование является частью длинной цепи зависимостей (не в этом случае).

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