Иногда ассоциативность может использоваться для ослабления зависимостей данных, и мне было любопытно, насколько она может помочь. Я был довольно удивлен, узнав, что я могу получить коэффициент ускорения 4, вручную развернув тривиальный цикл, как в Java (сборка 1.7.0_51-b13), так и в C (gcc 4.4.3).
Так что либо я делаю что-то довольно глупо, либо компиляторы игнорируют мощный инструмент. Я начал с
int a = 0;
for (int i=0; i<N; ++i) a = M1 * a + t[i];
который вычисляет что-то близкое к String.hashCode()
(установите M1=31
и используйте char[]
). Вычисление довольно тривиально, а t.length=1000
занимает около 1,2 микросекунды на моем i5-2400 @3.10GHz (как на Java, так и на C).
Обратите внимание, что каждые два шага a
умножаются на M2 = M1*M1
и добавляют что-то. Это приводит к этому фрагменту кода
int a = 0;
for (int i=0; i<N; i+=2) {
a = M2 * a + (M1 * t[i] + t[i+1]); // <-- note the parentheses!
}
if (i < len) a = M1 * a + t[i]; // Handle odd length.
Это ровно в два раза быстрее первого фрагмента. Как ни странно, оставляя круглые скобки, он ест 20% ускорения. Как ни странно, это можно повторить, и фактор 3.8 может быть достигнут.
В отличие от java, gcc -O3
решает не разворачивать цикл. Это мудрый выбор, поскольку он никоим образом не помог (как показывает -funroll-all-loops
).
Итак, мой вопрос 1: что мешает такой оптимизации?
Googling не работает, я получил только "ассоциативные массивы" и "ассоциативные операторы".
Update
Я немного обработал benchmark и может предоставить некоторые результаты. Нет ускорения за разворачиванием 4 раза, вероятно, из-за умножения и сложения вместе с 4 циклами.
Обновление 2
Поскольку Java уже разворачивает цикл, вся тяжелая работа выполняется. Мы получаем что-то вроде
...pre-loop
for (int i=0; i<N; i+=2) {
a2 = M1 * a + t[i];
a = M1 * a2 + t[i+1];
}
...post-loop
где интересную часть можно переписать как
a = M1 * ((M1 * a) + t[i]) + t[i+1]; // latency 2mul + 2add
Это показывает, что есть 2 умножения и 2 дополнения, все они должны выполняться последовательно, таким образом, необходимо 8 циклов на современном процессоре x86. Все, что нам нужно сейчас, это математика начальной школы (работает для int
даже в случае переполнения или что-то еще, но не применимо к плавающей точке).
a = ((M1 * (M1 * a)) + (M1 * t[i])) + t[i+1]; // latency 2mul + 2add
Пока мы ничего не получили, но это позволяет нам сбрасывать константы
a = ((M2 * a) + (M1 * t[i])) + t[i+1]; // latency 1mul + 2add
и получить еще больше, перегруппировав сумму
a = (M2 * a) + ((M1 * t[i]) + t[i+1]); // latency 1mul + 1add