как оптимизировать этот код с коэффициентом развертывания 3?

void randomImprovedfunction(double a[], double p[], long n)
2 {
3      long i;
4      double last_v, v;
5      last_v = p[0] = a[0];
6      for (i=1; i<n; i++) {
7          v = last_v + a[i];
8          p[i] = v ;
9         last_v = v;
10     }
11
12     return ;
13 }

У меня есть эта функция. в качестве упражнения мне сказали, что его можно оптимизировать, используя коэффициент развертывания, равный 3, и изменяя только строки 7-9. Тем не менее, я действительно потерялся в том, как это будет сделано.

Может ли кто-нибудь показать это мне?

Благодарю вас! любая помощь приветствуется :)


person Megan Darcy    schedule 28.06.2021    source источник
comment
Я бы сделал это как for (x=1;x+3 < n; x+=3) ..., изменив также строку 6 (с необходимостью обработки остатка n%3 !=0). Изменение строк с 7 по 9 означает только то, что также n означает, что теперь кратно 3, а строки 7,8,9 необходимо скопировать три раза с правильным расчетом индекса.   -  person Aki Suihkonen    schedule 28.06.2021
comment
удалите номера строк и просто добавьте комментарии к строкам, о которых вы хотите поговорить Почему в разделах кода нет нумерации строк?< /а>   -  person phuclv    schedule 28.06.2021
comment
@AkiSuihkonen: Обычно вы хотите x < n-2 или x+2 < n. Или x+3 <= n. Я предпочитаю x < n-2, чтобы компилятору было проще поднять инвариант цикла, поскольку +2 не совпадает с приращением счетчика. <= может быть в порядке; UB со знаком-переполнением сообщает компилятору о завершении. Ваше управление циклом оставит невыполненными 1..3 элемента, а не 0..2. (Очевидное доказательство того, что ручное развертывание цикла сложно; даже опытные люди склонны ошибаться; лучше всего использовать clang -O3 и позволить ему разворачиваться, когда это целесообразно, потому что автоматическая векторизация обычно лучше работает с идиоматическими циклами).   -  person Peter Cordes    schedule 28.06.2021
comment
@AkiSuihkonen: Или вам нужно включить дополнительные i+=2 и if() break; в тело цикла, в основном обходя бессмысленное требование изменять только тело цикла, а не элемент управления. Если количество элементов массива не должно быть n*3 в развернутой версии, поэтому вы просто делаете i*3 + 0..2.   -  person Peter Cordes    schedule 28.06.2021
comment
Нет особого смысла разворачивать эту сумму префиксов, если только вы не собираетесь предполагать ассоциативный FP. Отдельные переменные v и last_v не приносят никакой пользы. Я бы упростил это до v += a[i]. (Что будет компилировать то же самое, но будет проще для людей.) Возможно, они хотят, чтобы вы предположили ассоциативную FP, поэтому вы могли бы сделать tmp = a[x + 0] + ... + a[x+2], чтобы переносимая циклом зависимость была просто v += tmp, с промежуточными v значениями, вычисленными из старого v , Таким образом, вы выполняете избыточную работу, чтобы избежать задержки.   -  person Peter Cordes    schedule 28.06.2021
comment
См. также параллельную префиксную (кумулятивную) сумму с SSE для более грубой силы с использованием перетасовки SIMD SSE/AVX.   -  person Peter Cordes    schedule 28.06.2021


Ответы (1)


Ваша основная цель с развертыванием — упростить обработку инструкций конвейером инструкций ЦП. Несколько инструкций могут выполняться одновременно, и различные факторы могут прерывать плавный поток. Например, зависимости данных: если более поздней инструкции необходимо загрузить данные, и эти данные изменяются более ранними инструкциями, более поздняя инструкция должна ждать на этапе загрузки, пока более ранние инструкции не сохранят эти данные. Это называется застой конвейера.

См. комментарии о том, почему зависимость от данных является основным узким местом в этом примере.

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

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

for (i=1; i < n; i+=3) {
    v = last_v + a[i];
    p[i] = v ;
    last_v = v;
    v = last_v + a[i+1];
    p[i+1] = v ;
    last_v = v;
    v = last_v + a[i+2];
    p[i+2] = v ;
    last_v = v;
}

Нежелательные случаи — обратите внимание, что последним индексом, который вы хотите обработать, является (n-1)

n Unwanted cases
5 Array indexes 1,2,3 then 4,5,6 => the unrolled code processes 2 unwanted cases, index 5 and 6
6 Array indexes 1,2,3 then 4,5,6 => the unrolled code processes 1 unwanted case, index 6
7 Array indexes 1,2,3 then 4,5,6 => no unwanted cases

См. также Обработка остатка развернутого цикла

Итак, удалите последний цикл, если есть какие-либо нежелательные случаи, и тогда у вас будет

// `nn` is the adjusted loop limit to avoid an extra loop with unwanted cases 
int nn = n;
if ( (n-1)%3 != 0 ) nn = n - 3;

for (i=1; i < nn; i+=3) {
    v = last_v + a[i];
    p[i] = v ;
    last_v = v;
    v = last_v + a[i+1];
    p[i+1] = v ;
    last_v = v;
    v = last_v + a[i+2];
    p[i+2] = v ;
    last_v = v;
}

На этом этапе нам нужно обработать оставшиеся/отсутствующие случаи:

n nn values of iterator i
5 2 1,4 => final i = n - 1
6 3 1,4 => final i = n - 2
7 7 1,4,7 => final i = n

Если i = n - 1, у вас есть 1 пропущенный случай, т.е. индекс n-1
Если i = n - 2, у вас есть 2 пропущенных случая, т.е. индекс n-2 и n-1
Если i = п, все готово

if ( i == n - 1 ) { // 1 missing case
    v = last_v + a[n-1]
    p[n-1] = v;
}
if ( i == n - 2 ) { // 2 missing cases
    v = last_v + a[n-2]
    p[n-2] = v;
    last_v = v;
    v = last_v + a[n-1]
    p[n-1] = v;
}
person John D    schedule 28.06.2021
comment
Ваша основная цель развертывания — уменьшить количество точек ветвления. — Это не является узким местом в этом цикле на современном процессоре, как в учебнике кверента (см. предыдущие вопросы, такие как как развертывание повлияет на количество циклов на количество элементов CPE). Узким местом является цепочка зависимостей FP. Сделать что-то с этим — это ключевой фактор, если только мы не настраиваемся на гиперпоточность и не хотим использовать меньше мопов, даже если мы не изменим узкое место в задержке. (См. также мои комментарии к вопросу об укорочении цепочки v += ... dep) - person Peter Cordes; 29.06.2021
comment
Кроме того, вам не нужно использовать %3, чтобы найти новую верхнюю границу цикла; поскольку мы используем < вместо вычисления точного условия завершения == (полезно только для MIPS), всегда работает nn = n - 2. С подписанным n вам даже не нужен специальный случай n < 2. (Также обсуждается в комментариях под вопросом) - person Peter Cordes; 29.06.2021
comment
@PeterCordes Я думал, что ОП был сбит с толку тем, что означает вопрос из учебника, поэтому пытался дать простой ответ, чтобы они могли в общих чертах увидеть, как работает развертывание. Я исправлю ветвление в преамбуле, как только прочитаю ваши ссылки. - person John D; 29.06.2021
comment
Да, ИДК, нужно ли кверенту просто изложить супер основы наивного разворота, или что. И это, вероятно, полезно в целом / в теории. Просто не ожидайте, что это сильно улучшит производительность на реальных процессорах. (Возможно, сделать что-то с последовательной зависимостью - это следующее упражнение в учебнике.) В этом ответе, безусловно, есть полезные вещи, особенно о правильном выполнении условия цикла: это постоянно встречается в циклах SIMD. - person Peter Cordes; 29.06.2021
comment
Другой момент заключается в том, что современные компиляторы могут разворачиваться за нас (особенно clang), а ручное развертывание может на самом деле победить это (или, что чаще, победить автоматическую векторизацию), по сравнению с более идиоматичным циклом. Но если ваш компилятор еще не работает даже лучше, чем любое ручное развертывание, которое вы придумали, иногда это может помочь. Некоторые циклы являются узким местом в пропускной способности интерфейса, а не в задержке. - person Peter Cordes; 29.06.2021