Ответ 1
Я протестировал "сокращенную" версию кода с помощью компилятора VS2012 C
int main()
{
int A[12] = { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 };
int sum = 0;
int i;
for (i = 0; i < 12; ++i)
sum += A[11 - i];
printf("%d\n", sum);
return 0;
}
Я скомпилировал его в режиме x64. Конфигурация выпуска оптимизирована для скорости. Ошибка все еще существует, но в зависимости от других настроек оптимизации и генерации кода она проявляется по-разному. Одна версия кода генерирует "случайный" результат, а другой последовательно вырабатывает 8
как сумму (вместо правильной 12
).
Вот как выглядит сгенерированный код для версии, которая последовательно создает 8
000000013FC81DF0 mov rax,rsp
000000013FC81DF3 sub rsp,68h
000000013FC81DF7 movd xmm1,dword ptr [rax-18h]
000000013FC81DFC movd xmm2,dword ptr [rax-10h]
000000013FC81E01 movd xmm5,dword ptr [rax-0Ch]
000000013FC81E06 xorps xmm0,xmm0
000000013FC81E09 xorps xmm3,xmm3
for (i = 0; i < 12; ++i)
000000013FC81E0C xor ecx,ecx
000000013FC81E0E mov dword ptr [rax-48h],1
000000013FC81E15 mov dword ptr [rax-44h],1
000000013FC81E1C mov dword ptr [rax-40h],1
000000013FC81E23 punpckldq xmm2,xmm1
000000013FC81E27 mov dword ptr [rax-3Ch],1
000000013FC81E2E mov dword ptr [rax-38h],1
000000013FC81E35 mov dword ptr [rax-34h],1
{
sum += A[11 - i];
000000013FC81E3C movdqa xmm4,xmmword ptr [[email protected] (013FC83360h)]
000000013FC81E44 paddd xmm4,xmm0
000000013FC81E48 movd xmm0,dword ptr [rax-14h]
000000013FC81E4D mov dword ptr [rax-30h],1
000000013FC81E54 mov dword ptr [rax-2Ch],1
000000013FC81E5B mov dword ptr [rax-28h],1
000000013FC81E62 mov dword ptr [rax-24h],1
000000013FC81E69 punpckldq xmm5,xmm0
000000013FC81E6D punpckldq xmm5,xmm2
000000013FC81E71 paddd xmm5,xmm3
000000013FC81E75 paddd xmm5,xmm4
000000013FC81E79 mov dword ptr [rax-20h],1
000000013FC81E80 mov dword ptr [rax-1Ch],1
000000013FC81E87 mov r8d,ecx
000000013FC81E8A movdqa xmm0,xmm5
000000013FC81E8E psrldq xmm0,8
000000013FC81E93 paddd xmm5,xmm0
000000013FC81E97 movdqa xmm0,xmm5
000000013FC81E9B lea rax,[rax-40h]
000000013FC81E9F mov r9d,2
000000013FC81EA5 psrldq xmm0,4
000000013FC81EAA paddd xmm5,xmm0
000000013FC81EAE movd edx,xmm5
000000013FC81EB2 nop word ptr [rax+rax]
{
sum += A[11 - i];
000000013FC81EC0 add ecx,dword ptr [rax+4]
000000013FC81EC3 add r8d,dword ptr [rax]
000000013FC81EC6 lea rax,[rax-8]
000000013FC81ECA dec r9
000000013FC81ECD jne main+0D0h (013FC81EC0h)
}
printf("%d\n", sum);
000000013FC81ECF lea eax,[r8+rcx]
000000013FC81ED3 lea rcx,[__security_cookie_complement+8h (013FC84040h)]
000000013FC81EDA add edx,eax
000000013FC81EDC call qword ptr [__imp_printf (013FC83140h)]
return 0;
000000013FC81EE2 xor eax,eax
}
000000013FC81EE4 add rsp,68h
000000013FC81EE8 ret
Там много странного и, казалось бы, ненужного mumbo-jumbo, оставленного генератором кода и оптимизатором, но то, что этот код делает, можно кратко описать следующим образом.
Существует два независимых алгоритма для получения конечной суммы, которые, по-видимому, должны обрабатывать разные части массива. Я предполагаю, что два потока обработки (не SSE и SSE) используются для продвижения parallelism посредством конвейерной обработки команд.
Один алгоритм - это простой цикл, который суммирует элементы массива, обрабатывая два элемента на итерацию. Он может быть извлечен из вышеуказанного "чередующегося" кода следующим образом
; Initialization
000000013F1E1E0C xor ecx,ecx ; ecx - odd element sum
000000013F1E1E87 mov r8d,ecx ; r8 - even element sum
000000013F1E1E9B lea rax,[rax-40h] ; start from i = 2
000000013F1E1E9F mov r9d,2 ; do 2 iterations
; The cycle
000000013F1E1EC0 add ecx,dword ptr [rax+4] ; ecx += A[i + 1]
000000013F1E1EC3 add r8d,dword ptr [rax] ; r8d += A[i]
000000013F1E1EC6 lea rax,[rax-8] ; i -= 2
000000013F1E1ECA dec r9
000000013F1E1ECD jne main+0D0h (013F1E1EC0h) ; loop again if r9 is not zero
Этот алгоритм начинает добавлять элементы из адреса rax - 40h
, который в моем эксперименте был равен &A[2]
и делает две итерации назад, пропуская назад два элемента. Это скопирует сумму A[0]
и A[2]
в регистре r8
и сумму A[1]
и A[3]
в регистре ecx
. Таким образом, эта часть алгоритма обрабатывает 4 элемента массива и правильно генерирует значения 2
как в r8
, так и в ecx
.
Другая часть алгоритма написана с использованием инструкций SSE и, по-видимому, отвечает за суммирование оставшейся части массива. Его можно извлечь из кода следующим образом
; Initially xmm5 is zero
000000013F1E1E3C movdqa xmm4,xmmword ptr [[email protected] (013F1E3360h)]
000000013F1E1E75 paddd xmm5,xmm4
000000013F1E1E8A movdqa xmm0,xmm5 ; copy
000000013F1E1E8E psrldq xmm0,8 ; shift
000000013F1E1E93 paddd xmm5,xmm0 ; and add
000000013F1E1E8A movdqa xmm0,xmm5 ; copy
000000013F1E1E8E psrldq xmm0,4 ; shift
000000013F1E1E93 paddd xmm5,xmm0 ; and add
000000013F1E1EAE movd edx,xmm5 ; edx - the sum
Общий алгоритм, используемый этой частью, прост: он помещает значение 0x00000001000000010000000100000001
в 128-битный регистр xmm5
, затем сдвигает его на 8 байтов вправо (0x00000000000000000000000100000001
) и добавляет его к исходному значению, производя 0x00000001000000010000000200000002
. Это снова смещается на 4 байта вправо (0x00000000000000010000000100000002
) и снова добавляется к предыдущему значению, создавая 0x00000001000000020000000300000004
. Последнее 32-битное слово 0x00000004
of xmm5
взято в качестве результата и помещено в регистр edx
. Таким образом, этот алгоритм дает 4
как его конечный результат. Совершенно очевидно, что этот алгоритм просто выполняет "параллельное" добавление последовательных 32-битных слов в 128-битном регистре. Обратите внимание, BTW, что этот алгоритм даже не пытается получить доступ к A
, он начинает суммирование из встроенной константы, созданной компилятором/оптимизатором.
Теперь, в конце, значение r8 + ecx + edx
сообщается как окончательная сумма. Очевидно, это только 8
, а не правильный 12
. Похоже, что один из этих двух алгоритмов забыл сделать некоторые из своих работ. Я не знаю, какой из них, но, судя по обилию "избыточных" инструкций, похоже, что это был алгоритм SSE, который должен был генерировать 8
в edx
вместо 4
. Одна подозрительная инструкция - это
000000013FC81E71 paddd xmm5,xmm3
В этот момент xmm3
всегда содержит ноль. Таким образом, эта инструкция выглядит полностью избыточной и ненужной. Но если xmm3
на самом деле содержала другую "магическую" константу, представляющую еще 4 элемента массива (так же, как и xmm4
), тогда алгоритм работал бы правильно и создавал бы правильную сумму.
Если для элементов массива используются отличительные начальные значения
int A[12] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
ясно видно, что первый (не SSE) алгоритм успешно суммирует 1, 2, 3, 4
, а второй (SSE) алгоритм суммирует 9, 10, 11, 12
. 5, 6, 7, 8
остаются исключенными из рассмотрения, в результате 52
в качестве окончательной суммы вместо правильного 78
.
Это определенно ошибка компилятора/оптимизатора.
P.S. Тот же проект с теми же настройками, которые импортированы в VS2013 Update 2, похоже, не страдает от этой ошибки.