Ошибка в компиляторе 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 о том, как возникла проблема.

Другие вопросы по тегам