2013-09-08 2 views
3
double diff, dsq = 0; 
double *descr1, *descr2; 
int i, d; 

for (i = 0; i < d; ++i) 
{ 
    diff = descr1[i] - descr2[i]; 
    dsq += diff * diff; 
} 
return dsq; 

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

+0

Можете ли вы переключиться на целые числа? Какой диапазон значений будет охватываться содержимым 'descr1/2'? – alk

+9

Вы не инициализировали d! –

+0

Если зависимости позволяют запускать ядро ​​на графическом процессоре – jev

ответ

0

Поместите оба номера descr1 и descr2 в структуру, чтобы они находились рядом друг с другом в памяти. Это сделало бы использование кеша и доступ к памяти лучше.

использовать также register для diff и dsq

+6

Использование регистра бесполезно, компиляторы игнорируют его –

+1

Я бы сказал то же самое несколько иначе: если у вас есть своего рода компилятор, который нуждается в подсказках 'register', самый большой ускорение скорости, которое вы можете получить бесплатно, - это обновление до состояния, компилятор. –

+0

@GregoryPakosz - Это подсказка для компилятора. Любая ссылка, чтобы объяснить, что она игнорируется всеми компиляторами? –

2

Вы могли бы помочь компилятору немного со своими строгими правилами наложения спектров:

double calc_ssq(double *restrict descr1, double *restrict descr2, size_t count) 
{ 
double ssq; 

ssq = 0.0; 
for (;count; count--) { 
     double diff; 
     diff = *descr1++ - *descr2++; 
     ssq += diff * diff; 
     } 
return ssq; 
} 
+0

Не совсем, нет никакого доступа к записи через любой указатель кода, поэтому наложение не может остановить компилятор от оптимизации исходного кода. Если это влияет на производительность, это указывает только на то, что компилятор недостаточно умен. – cmaster

+0

В исходном фрагменте кода нет, но вполне возможно, что фактический код достаточно сложный, что анализ сглаживания не может быть выполнен. – Sneftel

+0

Прошу прощения за этот взнос, он был предназначен только для того, чтобы обсудить. Может быть, 'const' будет делать то же самое здесь, так как массивы никогда не записываются. Я не специалист по подобным вещам, но у меня такое ощущение, что такой механизм был движущей силой для введения этой функции на C99. (вставьте сюда цитату dmr ...) Кстати: вложение, вероятно, тоже поможет. – wildplasser

1

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

Я полагаю, что однократное умножение будет быстрее, чем умножение двойной точности в случае 32-битного процессора, так как регулярному float нужен только один регистр процессора, а double нужны два.

Я не уверен, что кастинг не «поедает» все улучшения скорости, что вы получите от одномерного умножения.

+0

Кастинг не будет улучшать скорость, потому что поплавки всегда отображаются как 'long double' в регистры с плавающей запятой, приведение - это noop. Разница заключается в том, что компилятор будет использовать варианты одиночной точности арифметических команд вместо двухступенчатых, которые обычно выполняются в два раза быстрее. – cmaster

+0

@cmaster не знал, что о 'long double'. Благодарю. – Alex

1

Если d равномерно делится на 2, я хотел бы попробовать что-то вроде этого:

for(i=0;i<d;i+=2) 
{ 
    diff0 = descr1[i] - descr2[i]; 
    diff1 = descr1[i+1] - descr2[i+1]; 
    dsq += diff0 * diff0 + diff1 * diff1; 
} 

Каких бы намекает на оптимизатор, что можно чередовать шесть операций. Даже если d было нечетным, вы могли бы добавить значение 0.0 в конец каждого вектора (давая четное количество значений), так как это не имело бы никакого отношения к результату, учитывая задействованные операции.

Следующий шаг может быть дополнением к векторам, которые должны быть равномерно делимы на четыре, выполняя четыре вычитания, четыре умножения и четыре дополнения перед итерацией с i + = 4;

нацело на восемь позволяет векторы точно соответствовать размеру кэш-строки 64.

с плавающей точкой умножений требуется только тактовый цикл или два, чтобы завершить как сделать сложение и вычитание (в соответствии с Agner тумана). Таким образом, для вашего примера сокращение расходов на итерацию должно ускорить работу.

+0

Я должен сказать, если бы этот код ускорил вашу программу, то устройство Даффа (в моем ответе) предложит хотя бы 4-кратное увеличение. Вы действительно должны попробовать. – abelenky

+0

@abalenky: Я не убежден, потому что оптимизатор может или (возможно) не может понять, что такое чередование операций. По сути, каждый случай должен быть атомарным, поэтому случай 7 не может чередовать с оптимизациями из случая 0, случай 6 из 7 и т. Д. Каждое промежуточное значение потребует хранения отдельно до этапа суммирования. Как я вижу, вы, по сути, заменяете накладные расходы на коммутатор с накладными расходами на коммутаторе. –

+0

Я должен сказать, если бы этот код ускорил вашу программу, то с использованием SSE/AVX и нескольких ядер (наряду с разворачиванием цикла, который этот ответ сделал) ускорит его еще немного. –

3

Это определенно случай для Duff's Device.

Вот моя реализация, основанная на устройстве Даффа.
(Примечание: только слегка протестирован ... это должен наступали через в отладчике, чтобы обеспечить правильное поведение)

void fnc(void) 
{ 
    double dsq = 0.0; 
    double diff[8] = {0.0}; 
    double descr1[115]; 
    double descr2[115]; 
    double* pD1 = descr1; 
    double* pD2 = descr2; 
    int d = 115; 

    //Fill with random data for testing 
    for(int i=0; i<d; ++i) 
    { 
     descr1[i] = (double)rand()/(double)rand(); 
     descr2[i] = (double)rand()/(double)rand(); 
    } 

    // Duff's Device: Step through this in a debugger, its AMAZING. 
    int c = (d + 7)/8; 
    switch(d % 8) { 
    case 0: do { diff[0] = *pD1++ - *pD2++; diff[0] *= diff[0]; 
    case 7:   diff[7] = *pD1++ - *pD2++; diff[7] *= diff[7]; 
    case 6:   diff[6] = *pD1++ - *pD2++; diff[6] *= diff[6]; 
    case 5:   diff[5] = *pD1++ - *pD2++; diff[5] *= diff[5]; 
    case 4:   diff[4] = *pD1++ - *pD2++; diff[4] *= diff[4]; 
    case 3:   diff[3] = *pD1++ - *pD2++; diff[3] *= diff[3]; 
    case 2:   diff[2] = *pD1++ - *pD2++; diff[2] *= diff[2]; 
    case 1:   diff[1] = *pD1++ - *pD2++; diff[1] *= diff[1]; 
        dsq += diff[0] + diff[1] + diff[2] + diff[3] + diff[4] + diff[5] + diff[6] + diff[7]; 
       } while(--c > 0); 
    } 
} 


Объяснение
Как уже сказал, есть немного вы можете сделать, чтобы оптимизировать операции с плавающей запятой. Однако в вашем исходном коде программа потратила много времени на проверку значения i.

Этапы выполнения были грубо:

Is i < d? ==> Yes 
Do some math. 
Is i < d? ==> Yes 
Do some math. 
Is i < d? ==> Yes 
Do some math. 
Is i < d? ==> Yes 
Do some math. 

Вы можете видеть каждый шаг проверки i.

С устройством Даффа вы получаете восемь операций перед проверкой счетчика (c в этом случае).

Теперь шаги выполнения примерно:

Is c > 0? ==> Yes 
Do some math. 
Do some math. 
Do some math. 
Do some math. 
Do some math. 
Do some math. 
Do some math. 
Do some math. 
Is c > 0? ==> Yes 
Do some math. 
Do some math. 
Do some math. 
Do some math. 
Do some math. 
Do some math. 
Do some math. 
Do some math. 
Is c > 0? ==> Yes 
[...] 

В других случаях, вы тратите около 8 раза больше CPU на самом деле выполнения работы, и гораздо меньше времени, проверяя значение вашего счетчика. Это победа BIG.

Я подозреваю, что вы даже можете развернуть цикл до 16 или 32 операций даже для большей победы. Это действительно зависит от вероятных значений d в вашем коде.

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

+0

Устройство Duff, по-видимому, было реализовано для копирования из одного места памяти в другое, что намного проще в оптимизации упражнений, чем в арифметике в вопросе OP. Кроме того, D's d был изобретен в 1983 году, когда футеровка трубопроводов находилась в зачаточном состоянии на 80286 и несуществующей (?) На 68k. Таким образом, «определенно» было уместно в 1983 году, но, возможно, менее сегодня, учитывая 30-летнюю разработку HW. Компиляторы C умнее сегодня, если вы используете сценарий оптимизации, который они распознают, но в остальном они менее глупы. –

1

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

Таким образом, единственная реальная надежда для оптимизации, что у вас есть: а) использовать массивы float вместо массивов double сократить объем данных, загружаемых из половины памяти, и б) не вызывать этот код как можно больше ,

Вот некоторые цифры:

У вас есть три двойных арифметических инструкций в вашем внутреннем цикле, что примерно 6 циклов. Они нуждаются в 16 байтах данных. На 3 ГГц процессоре это пропускная способность 8 ГБ/с. Модуль DDR3-1066 обеспечивает 8,5 ГБ/с. Таким образом, даже если вы используете SSE и прочее, вы не получите намного быстрее, если не переключитесь на использование float.

+0

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

0

Предупреждение: Неподтвержденный код ниже.

Если ваше оборудование и компилятор поддерживают их, вы можете захотеть скомпоновать некоторые из операций с использованием векторов. В прошлом я использовал нечто похожее на следующее: в компиляторе GCC 4.6.x (машина x86-64 Ubuntu). Некоторые из синтаксиса могут быть незначительными или могут отличаться при использовании другого компилятора/архитектуры. Однако я должен быть достаточно близок, чтобы немного продвинуться к вашей цели.

typedef double v2d_t __attribute__((vector_size (16))); 

typedef union { 
    v2d_t vector; 
    double d[2]; 
} v2d_u; 

v2d_u vdsq = (v2d_t) {0.0, 0.0}; /* sum of square of differences */ 
v2d_u vdiff;    /* difference */ 
v2d_t * vdescr1;   /* pointer to array of aligned vector of doubles */ 
v2d_t * vdescr2;   /* pointer to array of aligned vector of doubles */ 
int  i;     /* index into array of aligned vector of doubles */ 
int  d;     /* # of elements in array */ 

/* 
* ... 
* Assuming that <d> is getting initialized appropriately somewhere 
* ... 
*/ 

for (i = 0; i < d; i++) { 
    vdiff.vector = vdescr1[i] - vdescr2[i]; 
    vdsq.vector += vdiff.vector * vdiff.vector; 
} 

return vdsq.d[0] + vdsq.d[1]; 

Возможно, вышеизложенное можно улучшить, чтобы повысить производительность. Возможно, несколько циклов разворачиваются. В качестве альтернативы, если вы можете использовать 256-битные векторы (например, YMMx на некоторых процессорах x86) вместо 128-битных векторов, которые использует этот образец, это тоже может ускорить процесс (потребуется некоторая настройка на код).

Надеюсь, это поможет.

1

Предполагая, что вы используете современный процессор Intel/AMD (с AVX), и вы хотите сохранить тот же алгоритм, вы можете попробовать следующий код. Он использует AVX и OpenMP для распараллеливания. Скомпилируйте с помощью GCC foo.c -mavx -fopenmp -O3. Если вы не хотите использовать OpenMP, просто закомментируйте два оператора #pragma.

Скорость будет зависеть от размеров массива и размеров кеша. Для массивов, которые вписываются в кеш L1, вы можете ожидать около 6-кратного ускорения (вы должны отключить OpenMP, а затем из-за его накладных расходов). Повышение будет продолжаться с каждым уровнем кеша. Когда он добирается до системной памяти, он все равно получает импульс, хотя (работа над удвоением 10M (2 * 80 МБ) по-прежнему на 70% выше по сравнению с моей двухжильной мостовой системой плюща).

#include <stdio.h> 
#include <stdlib.h> 
#include <immintrin.h> 
#include <omp.h> 

double foo_avx_omp(const double *descr1, const double *descr2, const int d) { 
    double diff, dsq = 0; 
    int i; 
    int range; 
    __m256d diff4_v1, diff4_v2, dsq4_v1, dsq4_v2, t1, l1, l2, l3, l4; 
    __m128d t2, t3; 
    range = (d-1) & -8; //round down to multiple of 8 
    #pragma omp parallel private(i,l1,l2,l3,l4,t1,t2,t3,dsq4_v1,dsq4_v2,diff4_v1,diff4_v2) \ 
    reduction(+:dsq) 
    { 
     dsq4_v1 = _mm256_set1_pd(0.0); 
     dsq4_v2 = _mm256_set1_pd(0.0); //two sums to unroll the loop once 

     #pragma omp for 
     for(i=0; i<(range/8); i++) { 
      //load one cache line of descr1 
      l1 = _mm256_load_pd(&descr1[8*i]); 
      l3 = _mm256_load_pd(&descr1[8*i+4]); 
      //load one cache line of descr2 
      l2 = _mm256_load_pd(&descr2[8*i]); 
      l4 = _mm256_load_pd(&descr2[8*i+4]); 
      diff4_v1 = _mm256_sub_pd(l1, l2); 
      diff4_v2 = _mm256_sub_pd(l3, l4); 
      dsq4_v1 = _mm256_add_pd(dsq4_v1, _mm256_mul_pd(diff4_v1, diff4_v1)); 
      dsq4_v2 = _mm256_add_pd(dsq4_v2, _mm256_mul_pd(diff4_v2, diff4_v2)); 
     } 
     dsq4_v1 = _mm256_add_pd(dsq4_v1, dsq4_v2); 
     t1 = _mm256_hadd_pd(dsq4_v1,dsq4_v1); 
     t2 = _mm256_extractf128_pd(t1,1); 
     t3 = _mm_add_sd(_mm256_castpd256_pd128(t1),t2); 
     dsq += _mm_cvtsd_f64(t3); 
    } 

    //finish remaining elements if d was not a multiple of 8 
    for (i=range; i < d; ++i) { 
     diff = descr1[i] - descr2[i]; 
     dsq += diff * diff; 
    } 
    return dsq; 
} 

double foo(double *descr1, double *descr2, int d) { 
    double diff, dsq = 0; 
    int i; 

    for (i = 0; i < d; ++i) 
    { 
     diff = descr1[i] - descr2[i]; 
     dsq += diff * diff; 
    } 
    return dsq; 
} 

int main(void) 
{ 
    double result1, result2, result3, dtime; 
    double *descr1, *descr2; 
    const int n = 2000000; 
    int i; 
    int repeat = 1000; 

    descr1 = _mm_malloc(sizeof(double)*n, 64); //align to a cache line 
    descr2 = _mm_malloc(sizeof(double)*n, 64); //align to a cache line 

    for(i=0; i<n; i++) { 
     descr1[i] = 1.0*rand()/RAND_MAX; 
     descr2[i] = 1.0*rand()/RAND_MAX; 
    } 
    dtime = omp_get_wtime(); 
    for(i=0; i<repeat; i++) { 
     result1 = foo(descr1, descr2, n); 
    } 
    dtime = omp_get_wtime() - dtime; 
    printf("foo %f, time %f\n", result1, dtime); 

    dtime = omp_get_wtime(); 
    for(i=0; i<repeat; i++) { 
     result1 = foo_avx_omp(descr1, descr2, n); 
    } 
    dtime = omp_get_wtime() - dtime; 
    printf("foo_avx_omp %f, time %f\n", result1, dtime); 

    return 0; 
} 
1

Похоже, вы вычисляете среднюю квадратичную ошибку двух векторов.

Используйте BLAS, и вы сможете использовать ручной оптимизированный код, который намного эффективнее, чем любой из нас когда-либо писал.

+0

Проверьте ответ мастера. Вы должны загрузить две 8-байтовые сущности для каждого цикла. PC3-12800 (cmaster использует PC3-8500) будет читать эквивалент 200 миллионов строк кэша в секунду. Так что 1600 миллионов 8-байтных сущностей в секунду поддерживаются через оперативную память и кеши. Вам нужно два для каждой итерации и выполнить три операции (вычесть, умножить и добавить) на них. 800 миллионов итераций в секунду для меня не кажется невозможным. Я не уверен, что ручной код будет «намного эффективнее». Но я думаю, это зависит от того, как вы определяете «гораздо больше». –

+0

Конечно. Не стесняйтесь брать проблему с «гораздо более эффективным». Моя первоначальная точка зрения заключается в том, что ОП должен воспользоваться той работой, которая была раньше. Не изобретайте велосипед, если, конечно, вы действительно не хотите стать колесником. –

+0

Почему бы не «изобретать велосипед»? Кодовые последовательности постоянно заново изобретаются из-за внешних факторов, позволяющих обрабатывать больше данных, новых алгоритмов или иногда просто для простой, хорошей старомодной забавы изучения того, как что-то тикает. –

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