Подтвердить что ты не робот

Различные версии Visual Studio 2012 Режим Release/Debug

Этот код создает разные значения в MSVS 2012, Windows 7, при переключении между режимами Debug и Release:

#include <iostream>
using namespace std;

int A[20000];

int main() {

    int shift = 0;
    int Period = 30;
    //Fill array
    for(int i = 0; i < 20000; i++) {
        A[i] = i * 2 + 123;
    }

    int sumTotal = 0;
    int sum = 0;

    for(int bars = Period + 10; bars < 1000; bars++) {
        sum = 0;
        for(int i = 0; i< Period; i++) {
            sum += A[bars - i];
        }
        sumTotal += sum;
    }
    cout << sumTotal << endl;
}

Можете ли вы воспроизвести или найти причину? Я тестировал все настройки параметров проекта.

  • Отладка (правильный результат): 32630400
  • Релиз: 32814720

/GS /GL /analyze- /W3 /Gy /Zc:wchar_t /I"C:\Program Files (x86)\Visual Leak Detector\include" /Z7 /Gm- /O2 /Fd"Release\vc110.pdb" /fp:precise /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /errorReport:prompt /WX- /Zc:forScope /Gd /Oy- /Oi /MD /Fa"Release\" /EHsc /nologo /Fo"Release\" /Fp"Release\Testing.pch"

4b9b3361

Ответ 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, похоже, не страдает от этой ошибки.

Ответ 2

Я считаю, что вы нашли ошибку в оптимизаторе. Вы можете получить сборку релиза, чтобы дать тот же (правильный) вывод в виде отладочной сборки, либо отключив оптимизацию, либо добавив дополнительный код с побочными эффектами, которые невозможно оптимизировать (например, cout << "hi") внутри самого внутреннего цикла for (это, по-видимому, предотвращает неправильную работу оптимизации в противном случае). Я бы предложил сообщить об этом Microsoft.


Обновление: Microsoft подтверждает, что это ошибка, связанная с автоматической векторизацией, и что она исправлена ​​в обновлении VS2013. 2. Обходной путь в другие версии - отключить векторизацию, префикс цикла с помощью #pragma loop(no_vector).

Далее, они описывают две разные конструкции цикла, которые могут вызвать ошибку. Я просто процитирую их:

Есть два случая, когда ошибка срабатывает:

1) В качестве упоминания пользователя, петли, которые становятся векторизованными из форма:

for (int я = 0;...) {sum = A [...] - sum; }

2) Циклы, которые векторизуются в форме:

for (int я = 0;...) {sum = sum + A [- i]; }

Они также предлагают следующее предложение для определения уязвимого кода:

Если вы просматриваете исходный код, чтобы попытаться найти эти я рекомендую начинать, бросая /Qvec -report: 1, чтобы найти все петли, которые были векторизованы, и оттуда. Чтобы обойти ошибок, поставьте #pragma loop (no_vector) над контурами for.

Ответ 3

Код, который дает ошибку оптимизации, можно свести к следующему:

#include <iostream>
using namespace std;

#define SIZE 12

int main()
{
    int A[SIZE] = {0};

    int sum = 0;
    for (int i=0; i<SIZE; i++)
        sum += A[SIZE-1-i];
    cout << sum << endl;

    return 0;
}

Ошибка оптимизации можно удалить, применив один из следующих изменений:

  • Измените определение SIZE на значение ниже 12
  • Измените выражение A[SIZE-1-i] на A[SIZE-i-1]
  • Переместите операцию cout << sum << endl в цикл

Итак, чтобы диагностировать проблему, мы можем просто применить одно из этих изменений, а затем сравнить между дизассемблированием кода перед изменением и дизассемблированием кода после изменения.

Ответ 4

Я сравнил код asm для обоих случаев (в VС++ 2013 express), в сборке релиза, код asm в сборке выпуска цикла for

for (int i = 0; i< Period; i++)

ниже, и он очень отличается от того, что в сборке отладки

[email protected]:

; 23   :        sum = 0;
; 24   :        for (int i = 0; i< Period; i++){

    xorps   xmm5, xmm5
    lea eax, DWORD PTR [edi+88]
    xorps   xmm4, xmm4
    mov ecx, 3
    npad    2
[email protected]:

; 25   :            //cout << "hi";
; 26   :            sum += A[bars - i];

    movd    xmm2, DWORD PTR [eax-4]
    lea eax, DWORD PTR [eax-32]
    movd    xmm0, DWORD PTR [eax+32]
    movd    xmm1, DWORD PTR [eax+36]
    movd    xmm3, DWORD PTR [eax+40]
    punpckldq xmm3, xmm0
    movd    xmm0, DWORD PTR [eax+48]
    punpckldq xmm1, xmm2
    movd    xmm2, DWORD PTR [eax+44]
    punpckldq xmm3, xmm1
    movd    xmm1, DWORD PTR [eax+52]
    paddd   xmm5, xmm3
    movd    xmm3, DWORD PTR [eax+56]
    punpckldq xmm3, xmm0
    punpckldq xmm1, xmm2
    punpckldq xmm3, xmm1
    paddd   xmm4, xmm3
    dec ecx
    jne SHORT [email protected]

; 23   :        sum = 0;
; 24   :        for (int i = 0; i< Period; i++){

    paddd   xmm4, xmm5
    xor edx, edx
    movdqa  xmm0, xmm4
    mov eax, edi
    psrldq  xmm0, 8
    mov esi, 3
    paddd   xmm4, xmm0
    movdqa  xmm0, xmm4
    psrldq  xmm0, 4
    paddd   xmm4, xmm0
    movd    ebx, xmm4
    npad    7
[email protected]:

; 25   :            //cout << "hi";
; 26   :            sum += A[bars - i];

    add ecx, DWORD PTR [eax]
    lea eax, DWORD PTR [eax-8]
    add edx, DWORD PTR [eax+4]
    dec esi
    jne SHORT [email protected]

; 27   :    

}

Как вы можете из кода asm, здесь используются инструкции SSE. Поэтому я проверил параметры компилятора для инструкций SSE в VС++, затем я указал /arch: IA32, чтобы отключить генерацию инструкций SSE и SSE2 для процессоров x86 в релиз сборки, то я получил тот же результат, что и сборка отладки.

Я не знаком с SSE, надеюсь, кто-то может объяснить больше, основываясь на моих выводах.