Ошибка в компиляторе VC++ 14.0 (2015)?
Я сталкивался с некоторыми проблемами, которые возникали только в режиме выпуска x86, а не во время выпуска x64 или любого режима отладки. Мне удалось воспроизвести ошибку, используя следующий код:
#include <stdio.h>
#include <iostream>
using namespace std;
struct WMatrix {
float _11, _12, _13, _14;
float _21, _22, _23, _24;
float _31, _32, _33, _34;
float _41, _42, _43, _44;
WMatrix(float f11, float f12, float f13, float f14,
float f21, float f22, float f23, float f24,
float f31, float f32, float f33, float f34,
float f41, float f42, float f43, float f44) :
_11(f11), _12(f12), _13(f13), _14(f14),
_21(f21), _22(f22), _23(f23), _24(f24),
_31(f31), _32(f32), _33(f33), _34(f34),
_41(f41), _42(f42), _43(f43), _44(f44) {
}
};
void printmtx(WMatrix m1) {
char str[256];
sprintf_s(str, 256, "%.3f, %.3f, %.3f, %.3f", m1._11, m1._12, m1._13, m1._14);
cout << str << "\n";
sprintf_s(str, 256, "%.3f, %.3f, %.3f, %.3f", m1._21, m1._22, m1._23, m1._24);
cout << str << "\n";
sprintf_s(str, 256, "%.3f, %.3f, %.3f, %.3f", m1._31, m1._32, m1._33, m1._34);
cout << str << "\n";
sprintf_s(str, 256, "%.3f, %.3f, %.3f, %.3f", m1._41, m1._42, m1._43, m1._44);
cout << str << "\n";
}
WMatrix mul1(WMatrix m, float f) {
WMatrix out = m;
for (unsigned int i = 0; i < 4; i++) {
for (unsigned int j = 0; j < 4; j++) {
unsigned int idx = i * 4 + j; // critical code
*(&out._11 + idx) *= f; // critical code
}
}
return out;
}
WMatrix mul2(WMatrix m, float f) {
WMatrix out = m;
unsigned int idx2 = 0;
for (unsigned int i = 0; i < 4; i++) {
for (unsigned int j = 0; j < 4; j++) {
unsigned int idx = i * 4 + j; // critical code
bool b = idx == idx2; // critical code
*(&out._11 + idx) *= f; // critical code
idx2++;
}
}
return out;
}
int main() {
WMatrix m1(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);
WMatrix m2 = mul1(m1, 0.5f);
WMatrix m3 = mul2(m1, 0.5f);
printmtx(m1);
cout << "\n";
printmtx(m2);
cout << "\n";
printmtx(m3);
int x;
cin >> x;
}
В приведенном выше коде mul2 работает, но mul1 нет. mul1 и mul2 просто пытаются перебрать числа с плавающей точкой в WMatrix и умножить их на f, но способ, которым индексы mul1 (i*4+j) каким-то образом приводят к неверным результатам. Все, что делает mul2 по-другому, - это проверяет индекс перед его использованием, а затем работает (есть много других способов работы с индексом, чтобы заставить его работать). Обратите внимание, что если вы удалите строку "bool b = idx == idx2", то mul2 также обрывается...
Вот вывод:
1.000, 2.000, 3.000, 4.000
5.000, 6.000, 7.000, 8.000
9.000, 10.000, 11.000, 12.000
13.000, 14.000, 15.000, 16.000
0.500, 0.500, 0.375, 0.250
0.625, 1.500, 3.500, 8.000
9.000, 10.000, 11.000, 12.000
13.000, 14.000, 15.000, 16.000
0.500, 1.000, 1.500, 2.000
2.500, 3.000, 3.500, 4.000
4.500, 5.000, 5.500, 6.000
6.500, 7.000, 7.500, 8.000
Правильный вывод должен быть...
1.000, 2.000, 3.000, 4.000
5.000, 6.000, 7.000, 8.000
9.000, 10.000, 11.000, 12.000
13.000, 14.000, 15.000, 16.000
0.500, 1.000, 1.500, 2.000
2.500, 3.000, 3.500, 4.000
4.500, 5.000, 5.500, 6.000
6.500, 7.000, 7.500, 8.000
0.500, 1.000, 1.500, 2.000
2.500, 3.000, 3.500, 4.000
4.500, 5.000, 5.500, 6.000
6.500, 7.000, 7.500, 8.000
Я что-то пропустил? Или это на самом деле ошибка в компиляторе?
2 ответа
Это влияет только на 32-битный компилятор; На сборки x86-64 это не влияет независимо от настроек оптимизации. Однако вы видите проблему, проявляющуюся в 32-битных сборках, будь то оптимизация по скорости (/O2) или размеру (/O1). Как вы упомянули, он работает как положено в отладочных сборках с отключенной оптимизацией.
Предложение Виммеля об изменении упаковки, хотя и точной, не меняет поведения. (Код ниже предполагает, что упаковка правильно установлена на 1 для WMatrix
.)
Я не могу воспроизвести его в VS 2010, но могу в VS 2013 и 2015. У меня не установлен 2012. Это достаточно хорошо, чтобы позволить нам проанализировать разницу между объектным кодом, созданным двумя компиляторами.
Вот код для mul1
от VS 2010 ("рабочий" код):
(На самом деле, во многих случаях компилятор вставлял код из этой функции на сайт вызова. Но компилятор по-прежнему будет выводить файлы дизассемблирования, содержащие код, сгенерированный для отдельных функций, до встраивания. Вот что мы смотрим здесь, потому что он более загроможден. Поведение кода полностью эквивалентно, независимо от того, встроено оно или нет.)
PUBLIC mul1
_TEXT SEGMENT
_m$ = 8 ; size = 64
_f$ = 72 ; size = 4
mul1 PROC
___$ReturnUdt$ = eax
push esi
push edi
; WMatrix out = m;
mov ecx, 16 ; 00000010H
lea esi, DWORD PTR _m$[esp+4]
mov edi, eax
rep movsd
; for (unsigned int i = 0; i < 4; i++)
; {
; for (unsigned int j = 0; j < 4; j++)
; {
; unsigned int idx = i * 4 + j; // critical code
; *(&out._11 + idx) *= f; // critical code
movss xmm0, DWORD PTR [eax]
cvtps2pd xmm1, xmm0
movss xmm0, DWORD PTR _f$[esp+4]
cvtps2pd xmm2, xmm0
mulsd xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss DWORD PTR [eax], xmm1
movss xmm1, DWORD PTR [eax+4]
cvtps2pd xmm1, xmm1
cvtps2pd xmm2, xmm0
mulsd xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss DWORD PTR [eax+4], xmm1
movss xmm1, DWORD PTR [eax+8]
cvtps2pd xmm1, xmm1
cvtps2pd xmm2, xmm0
mulsd xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss DWORD PTR [eax+8], xmm1
movss xmm1, DWORD PTR [eax+12]
cvtps2pd xmm1, xmm1
cvtps2pd xmm2, xmm0
mulsd xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss DWORD PTR [eax+12], xmm1
movss xmm2, DWORD PTR [eax+16]
cvtps2pd xmm2, xmm2
cvtps2pd xmm1, xmm0
mulsd xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss DWORD PTR [eax+16], xmm1
movss xmm1, DWORD PTR [eax+20]
cvtps2pd xmm1, xmm1
cvtps2pd xmm2, xmm0
mulsd xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss DWORD PTR [eax+20], xmm1
movss xmm1, DWORD PTR [eax+24]
cvtps2pd xmm1, xmm1
cvtps2pd xmm2, xmm0
mulsd xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss DWORD PTR [eax+24], xmm1
movss xmm1, DWORD PTR [eax+28]
cvtps2pd xmm1, xmm1
cvtps2pd xmm2, xmm0
mulsd xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss DWORD PTR [eax+28], xmm1
movss xmm1, DWORD PTR [eax+32]
cvtps2pd xmm1, xmm1
cvtps2pd xmm2, xmm0
mulsd xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss DWORD PTR [eax+32], xmm1
movss xmm1, DWORD PTR [eax+36]
cvtps2pd xmm1, xmm1
cvtps2pd xmm2, xmm0
mulsd xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss DWORD PTR [eax+36], xmm1
movss xmm2, DWORD PTR [eax+40]
cvtps2pd xmm2, xmm2
cvtps2pd xmm1, xmm0
mulsd xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss DWORD PTR [eax+40], xmm1
movss xmm1, DWORD PTR [eax+44]
cvtps2pd xmm1, xmm1
cvtps2pd xmm2, xmm0
mulsd xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss DWORD PTR [eax+44], xmm1
movss xmm2, DWORD PTR [eax+48]
cvtps2pd xmm1, xmm0
cvtps2pd xmm2, xmm2
mulsd xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss DWORD PTR [eax+48], xmm1
movss xmm1, DWORD PTR [eax+52]
cvtps2pd xmm1, xmm1
cvtps2pd xmm2, xmm0
mulsd xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss DWORD PTR [eax+52], xmm1
movss xmm1, DWORD PTR [eax+56]
cvtps2pd xmm1, xmm1
cvtps2pd xmm2, xmm0
mulsd xmm1, xmm2
cvtpd2ps xmm1, xmm1
cvtps2pd xmm0, xmm0
movss DWORD PTR [eax+56], xmm1
movss xmm1, DWORD PTR [eax+60]
cvtps2pd xmm1, xmm1
mulsd xmm1, xmm0
pop edi
cvtpd2ps xmm0, xmm1
movss DWORD PTR [eax+60], xmm0
pop esi
; return out;
ret 0
mul1 ENDP
Сравните это с кодом для mul1
генерируется VS 2015:
mul1 PROC
_m$ = 8 ; size = 64
; ___$ReturnUdt$ = ecx
; _f$ = xmm2s
; WMatrix out = m;
movups xmm0, XMMWORD PTR _m$[esp-4]
; for (unsigned int i = 0; i < 4; i++)
xor eax, eax
movaps xmm1, xmm2
movups XMMWORD PTR [ecx], xmm0
movups xmm0, XMMWORD PTR _m$[esp+12]
shufps xmm1, xmm1, 0
movups XMMWORD PTR [ecx+16], xmm0
movups xmm0, XMMWORD PTR _m$[esp+28]
movups XMMWORD PTR [ecx+32], xmm0
movups xmm0, XMMWORD PTR _m$[esp+44]
movups XMMWORD PTR [ecx+48], xmm0
npad 4
$LL4@mul1:
; for (unsigned int j = 0; j < 4; j++)
; {
; unsigned int idx = i * 4 + j; // critical code
; *(&out._11 + idx) *= f; // critical code
movups xmm0, XMMWORD PTR [ecx+eax*4]
mulps xmm0, xmm1
movups XMMWORD PTR [ecx+eax*4], xmm0
inc eax
cmp eax, 4
jb SHORT $LL4@mul1
; return out;
mov eax, ecx
ret 0
?mul1@@YA?AUWMatrix@@U1@M@Z ENDP ; mul1
_TEXT ENDS
Сразу видно, насколько короче код. Очевидно, что оптимизатор стал намного умнее между VS 2010 и VS 2015. К сожалению, иногда источником "умов" оптимизатора является использование ошибок в вашем коде.
Глядя на код, который совпадает с циклами, вы можете видеть, что VS 2010 развертывает циклы. Все вычисления выполняются в потоке, чтобы не было ответвлений. Это то, что вы ожидаете от циклов с верхними и нижними границами, которые известны во время компиляции и, как в этом случае, достаточно малы.
Что случилось в VS 2015? Ну, это ничего не развернуло. Есть 5 строк кода, а затем условный переход JB
вернуться к началу цикла последовательности. Это само по себе мало что говорит. То, что выглядит очень подозрительным, это то, что он только зацикливается 4 раза (см. cmp eax, 4
заявление, которое устанавливает флаги прямо перед выполнением jb
эффективно продолжая цикл до тех пор, пока счетчик меньше 4). Ну, это могло бы быть хорошо, если бы это объединило две петли в одну. Давайте посмотрим, что он делает внутри цикла:
$LL4@mul1:
movups xmm0, XMMWORD PTR [ecx+eax*4] ; load a packed unaligned value into XMM0
mulps xmm0, xmm1 ; do a packed multiplication of XMM0 by XMM1,
; storing the result in XMM0
movups XMMWORD PTR [ecx+eax*4], xmm0 ; store the result of the previous multiplication
; back into the memory location that we
; initially loaded from
inc eax ; one iteration done, increment loop counter
cmp eax, 4 ; see how many loops we've done
jb $LL4@mul1 ; keep looping if < 4 iterations
Код считывает значение из памяти (значение размера XMM из местоположения, определенного ecx + eax * 4
) в XMM0
, умножает его на значение в XMM1
(который был установлен вне цикла, основываясь на f
параметр), а затем сохраняет результат обратно в исходную ячейку памяти.
Сравните это с кодом для соответствующего цикла в mul2
:
$LL4@mul2:
lea eax, DWORD PTR [eax+16]
movups xmm0, XMMWORD PTR [eax-24]
mulps xmm0, xmm2
movups XMMWORD PTR [eax-24], xmm0
sub ecx, 1
jne $LL4@mul2
Помимо другой последовательности управления циклом (это устанавливает ECX
до 4 вне цикла, вычитает 1 каждый раз и продолжает цикл до тех пор, пока ECX
!= 0), большая разница здесь - это фактические значения XMM, которыми он манипулирует в памяти. Вместо загрузки из [ecx+eax*4]
загружается из [eax-24]
(предварительно добавив 16 к EAX
).
Чем отличается mul2
? Вы добавили код для отслеживания отдельного индекса в idx2
, увеличивая его каждый раз через цикл. Теперь одного этого было бы недостаточно. Если вы закомментируете назначение bool
переменная b
, mul1
а также mul2
результат в идентичном объектном коде. Явно без сравнения idx
в idx2
, компилятор может вывести, что idx2
полностью не используется, и, следовательно, устранить его, поворачивая mul2
в mul1
, Но при таком сравнении компилятор, по-видимому, не может устранить idx2
и его присутствие очень немного меняет то, какие оптимизации считаются возможными для функции, что приводит к расхождению в выходных данных.
Теперь возникает вопрос: почему это происходит? Это ошибка оптимизатора, как вы сначала подозревали? Что ж, нет, и, как упоминали некоторые из комментаторов, никогда не должно быть вашего первого инстинкта обвинять компилятор / оптимизатор. Всегда предполагайте, что в вашем коде есть ошибки, если вы не можете доказать обратное. Это доказательство всегда будет включать в себя разборку и, предпочтительно, ссылки на соответствующие части языкового стандарта, если вы действительно хотите, чтобы вас воспринимали всерьез.
В этом случае Mystical уже прибил проблему. Ваш код демонстрирует неопределенное поведение, когда он делает *(&out._11 + idx)
, Это делает определенные предположения о расположении WMatrix
структура в памяти, которую вы не можете легально создать, даже после явной установки упаковки.
Вот почему неопределенное поведение является злом - оно приводит к тому, что код иногда работает, но в других случаях это не так. Он очень чувствителен к флагам компилятора, особенно к оптимизации, а также к целевым платформам (как мы видели в верхней части этого ответа). mul2
работает только случайно. И то и другое mul1
а также mul2
не правы. К сожалению, ошибка в вашем коде. Хуже того, компилятор не выдал предупреждение, которое могло бы предупредить вас о вашем использовании неопределенного поведения.
Если мы посмотрим на сгенерированный код, проблема довольно ясна. Игнорирование нескольких кусочков, которые не связаны с проблемой, mul1
производит код как это:
movss xmm1, DWORD PTR _f$[esp-4] ; load xmm1 from _11 of source
; ...
shufps xmm1, xmm1, 0 ; duplicate _11 across floats of xmm1
; ...
for ecx = 0 to 3 {
movups xmm0, XMMWORD PTR [dest+ecx*4] ; load 4 floats from dest
mulps xmm0, xmm1 ; multiply each by _11
movups XMMWORD PTR [dest+ecx*4], xmm0 ; store result back to dest
}
Таким образом, вместо умножения каждого элемента одной матрицы на соответствующий элемент другой матрицы, он умножает каждый элемент одной матрицы на _11
другой матрицы.
Хотя невозможно точно подтвердить, как это произошло (не просматривая исходный код компилятора), это, безусловно, согласуется с предположением @Mysticial о том, как возникла проблема.