не удалось оптимизировать кажущийся очевидным инвариант цикла (но волатильный классификатор сотворил магию)

Godbolt Link: https://godbolt.org/g/Hv6MAL

typedef int cell;

cell y;
const cell *phys_addr = (const cell*)0x12340;
int main() {
for (int i = 0; i < 20; i++) {
for (int j = 0; j < 30; j++) {
for (int k = 0; k < 50; k++) {
const cell *subarray = (&phys_addr[i] + phys_addr[i]/sizeof(cell));
const cell *subsubarray = (&subarray[j] + subarray[j]/sizeof(cell));
y = subsubarray[k];
}
}
}
}

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

int main() {
for (int i = 0; i < 20; i++) {
const cell *subarray = (&phys_addr[i] + phys_addr[i]/sizeof(cell));
for (int j = 0; j < 30; j++) {
const cell *subsubarray = (&subarray[j] + subarray[j]/sizeof(cell));
for (int k = 0; k < 50; k++) {
y = subsubarray[k];
}
}
}
}

но сборка, сгенерированная gcc 8.2 с -O3 -m32 как флаги есть:

  push ebp
push edi
push esi
push ebx
sub esp, 8
mov eax, DWORD PTR phys_addr
mov DWORD PTR [esp], 0
mov DWORD PTR [esp+4], eax
mov ebp, eax
.L4:
xor esi, esi
.L3:
lea edi, [0+esi*4]
xor eax, eax
.L2:
mov edx, DWORD PTR [ebp+0]
mov ecx, DWORD PTR [esp+4]
shr edx, 2
add edx, DWORD PTR [esp]
lea ebx, [ecx+edx*4]
lea edx, [eax+esi]
add eax, 1
mov ecx, DWORD PTR [ebx+edi]
shr ecx, 2
add edx, ecx
mov edx, DWORD PTR [ebx+edx*4]
mov DWORD PTR y, edx
cmp eax, 50
jne .L2
add esi, 1
cmp esi, 30
jne .L3
add DWORD PTR [esp], 1
mov eax, DWORD PTR [esp]
add ebp, 4
cmp eax, 20
jne .L4
add esp, 8
xor eax, eax
pop ebx
pop esi
pop edi
pop ebp
ret

Почему компилятор не перемещает subarray а также subsubarray расчет за пределами внутренних циклов?


случайный volatile делает магию

Я случайно добавил volatile чтобы DCE не избавился от всего кода, а затем каким-то образом из внутренних циклов были выведены инварианты цикла.

int main() {
for (int i = 0; i < 20; i++) {
for (int j = 0; j < 30; j++) {
for (int k = 0; k < 50; k++) {
const cell *subarray = (&phys_addr[i] + phys_addr[i]/sizeof(cell));
const cell *subsubarray = (&subarray[j] + subarray[j]/sizeof(cell));
volatile cell y = subsubarray[k];
}
}
}
return 0;
}

В основном это было не из-за y будучи локальной переменной с момента использования std::cout << subsubarray[k]; помешал оптимизации.

Сборка, сгенерированная gcc 8.2 с -O3 -m32 в качестве флагов для вышеупомянутого кода:

main:
push ebp
push edi
xor edi, edi
push esi
push ebx
sub esp, 20
mov ebp, DWORD PTR phys_addr
.L4:
mov eax, DWORD PTR [ebp+0+edi*4]
xor ecx, ecx
shr eax, 2
add eax, edi
lea ebx, [ebp+0+eax*4]
lea esi, [ebx+200]
.L3:
mov edx, DWORD PTR [ebx+ecx*4]
mov DWORD PTR [esp], ecx
shr edx, 2
add edx, ecx
sal edx, 2
lea eax, [ebx+edx]
add edx, esi
.L2:
mov ecx, DWORD PTR [eax]
add eax, 4
mov DWORD PTR [esp+16], ecx
cmp edx, eax
jne .L2
mov ecx, DWORD PTR [esp]
add ecx, 1
cmp ecx, 30
jne .L3
add edi, 1
cmp edi, 20
jne .L4
add esp, 20
xor eax, eax
pop ebx
pop esi
pop edi
pop ebp
ret

Инварианты цикла выталкиваются из внутренних циклов. Что сделал случайный volatile сделать, чтобы позволить GCC оптимизировать инварианты? Оптимизация не происходит, когда лязг 6.0.0.

3

Решение

Речь идет не о случайном изменчивом решении вашей проблемы — проблема глубже.

Как вы уже догадались, проблема действительно относится к «у»

Проверьте этот пример:

typedef int cell;

const cell *phys_addr = (const cell*)0x12340;

int main() {
cell y = 1;
for (int i = 0; i < 20; i++) {
for (int j = 0; j < 30; j++) {
for (int k = 0; k < 50; k++) {
const cell *subarray = (&phys_addr[i] + phys_addr[i]/sizeof(cell));
const cell *subsubarray = (&subarray[j] + subarray[j]/sizeof(cell));
y /= subsubarray[k];
}
}
}
return y;
}

Я использовал трюк с делением, чтобы избежать сложной оптимизации (gcc может оценивать все циклы и предоставлять y непосредственно в простом назначении; при использовании сложения, подстановки или умножения он также развернет самый внутренний цикл — пожалуйста, поиграйте в godbolt, чтобы посмотреть, как он выглядит)

Теперь разборка выглядит так:
https://godbolt.org/g/R1EGSb

main:
push ebp
push edi
push esi
push ebx
sub esp, 12
mov eax, DWORD PTR phys_addr
mov DWORD PTR [esp], 0
mov DWORD PTR [esp+4], eax
mov eax, 1
.L4:
mov esi, DWORD PTR [esp]
mov edi, DWORD PTR [esp+4]
mov edx, DWORD PTR [edi+esi*4]
mov DWORD PTR [esp+8], edx
shr edx, 2
add edx, esi
xor esi, esi
lea edi, [edi+edx*4]
lea ebp, [edi+200]
.L3:
mov ebx, DWORD PTR [edi+esi*4]
shr ebx, 2
add ebx, esi
sal ebx, 2
lea ecx, [edi+ebx]
add ebx, ebp
.L2:
cdq
idiv DWORD PTR [ecx]
add ecx, 4
cmp ebx, ecx
jne .L2
add esi, 1
cmp esi, 30
jne .L3
add DWORD PTR [esp], 1
mov edi, DWORD PTR [esp]
cmp edi, 20
jne .L4
add esp, 12
pop ebx
pop esi
pop edi
pop ebp
ret
phys_addr:
.long 74560

.L2 является самым внутренним циклом, поэтому код выглядит так, как ожидалось — subarray и subsubarray предварительно вычисляются ранее.

Поэтому вы можете задаться вопросом — почему, когда «у» локально, все в порядке, а когда глобально — нет.

Чтобы было ясно — «у» не должно быть объявлено в основном. Это можно сделать статичным, как это

static cell y;
const cell * __restrict__ phys_addr = (const cell*)0x12340;

или использовать пространство имен

namespace wtf{ cell y; }
const cell * __restrict__ phys_addr = (const cell*)0x12340;

и чем называется y как wtf :: y;

Все еще хорош.

Все сгущается до алиасинга. Чтобы увидеть это, давайте сначала изменим y на указатель:

typedef int cell;

cell *  y;
const cell *  phys_addr = (const cell*)0x12340;

int main() {
cell ylocal;
y = &ylocal;
for (int i = 0; i < 20; i++) {
for (int j = 0; j < 30; j++) {
for (int k = 0; k < 50; k++) {
const cell *subarray = (&phys_addr[i] + phys_addr[i]/sizeof(cell));
const cell *subsubarray = (&subarray[j] + subarray[j]/sizeof(cell));
*y /= subsubarray[k];
}
}
}
return *y;
}

Оптимизация без цикла снова ….

Можно предположить, что y и phys_addr перекрывают друг друга — запись y может изменить некоторые ячейки памяти, поэтому все словари должны быть рассчитаны с самыми последними данными (const в phys_addr означает, что только ваш указатель не должен изменять память, а не то, что он доступен только для чтения в глобальном масштабе) ,

Но если вы «пообещаете», что эти адреса не пересекаются, оптимизация вернется.

typedef int cell;

cell * __restrict__ y;
const cell * __restrict__ phys_addr = (const cell*)0x12340;

int main() {
cell ylocal;
y = &ylocal;
for (int i = 0; i < 20; i++) {
for (int j = 0; j < 30; j++) {
for (int k = 0; k < 50; k++) {
const cell *subarray = (&phys_addr[i] + phys_addr[i]/sizeof(cell));
const cell *subsubarray = (&subarray[j] + subarray[j]/sizeof(cell));
*y /= subsubarray[k];
}
}
}
return *y;
}

TL; DR;

Если вы используете указатели, компилятор может оказаться не в состоянии доказать, что адреса не являются псевдонимами и будут использовать безопасный путь. Если вы на 100% уверены, что они не используют ограничивать сообщить ему об этом факте.

2

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]