2016-12-22 5 views
34

Я немного поиграл с сборкой x86-64, пытаясь узнать больше о различных расширениях SIMD, доступных (MMX, SSE, AVX).Большие различия в генерации кода GCC при компиляции как C++ vs C

Чтобы увидеть, как различные C или C++-конструкции преобразуются в машинный код GCC, я использовал Compiler Explorer, что является превосходным инструментом.

Во время одной из моих «сессий игры» я хотел увидеть, как GCC может оптимизировать простую инициализацию времени выполнения целочисленного массива. В этом случае я попытался записать числа от 0 до 2047 в массив из 2048 целых без знака.

Код выглядит следующим образом:

unsigned int buffer[2048]; 

void setup() 
{ 
    for (unsigned int i = 0; i < 2048; ++i) 
    { 
    buffer[i] = i; 
    } 
} 

Если включить оптимизацию и AVX-512 инструкций -O3 -mavx512f -mtune=intel GCC 6.3 создает некоторые действительно умный код :)

setup(): 
     mov  eax, OFFSET FLAT:buffer 
     mov  edx, OFFSET FLAT:buffer+8192 
     vmovdqa64  zmm0, ZMMWORD PTR .LC0[rip] 
     vmovdqa64  zmm1, ZMMWORD PTR .LC1[rip] 
.L2: 
     vmovdqa64  ZMMWORD PTR [rax], zmm0 
     add  rax, 64 
     cmp  rdx, rax 
     vpaddd zmm0, zmm0, zmm1 
     jne  .L2 
     ret 
buffer: 
     .zero 8192 
.LC0: 
     .long 0 
     .long 1 
     .long 2 
     .long 3 
     .long 4 
     .long 5 
     .long 6 
     .long 7 
     .long 8 
     .long 9 
     .long 10 
     .long 11 
     .long 12 
     .long 13 
     .long 14 
     .long 15 
.LC1: 
     .long 16 
     .long 16 
     .long 16 
     .long 16 
     .long 16 
     .long 16 
     .long 16 
     .long 16 
     .long 16 
     .long 16 
     .long 16 
     .long 16 
     .long 16 
     .long 16 
     .long 16 
     .long 16 

Однако, когда я проверил, что бы быть сгенерированным, если тот же код был скомпилирован с использованием C-компилятора GCC, добавив флаги -x c. Я был очень удивлен.

Я ожидал, что аналогичные, если не идентичные, результаты, но С-компилятор, похоже, генерирует много более сложный и предположительно также гораздо более медленный машинный код. Полученная сборка слишком велика, чтобы вставить здесь полностью, но ее можно просмотреть на сайте godbolt.org, указав ссылку this.

Фрагмент сгенерированного кода, строки 58 до 83, можно увидеть ниже:

.L2: 
     vpbroadcastd zmm0, r8d 
     lea  rsi, buffer[0+rcx*4] 
     vmovdqa64  zmm1, ZMMWORD PTR .LC1[rip] 
     vpaddd zmm0, zmm0, ZMMWORD PTR .LC0[rip] 
     xor  ecx, ecx 
.L4: 
     add  ecx, 1 
     add  rsi, 64 
     vmovdqa64  ZMMWORD PTR [rsi-64], zmm0 
     cmp  ecx, edi 
     vpaddd zmm0, zmm0, zmm1 
     jb  .L4 
     sub  edx, r10d 
     cmp  r9d, r10d 
     lea  eax, [r8+r10] 
     je  .L1 
     mov  ecx, eax 
     cmp  edx, 1 
     mov  DWORD PTR buffer[0+rcx*4], eax 
     lea  ecx, [rax+1] 
     je  .L1 
     mov  esi, ecx 
     cmp  edx, 2 
     mov  DWORD PTR buffer[0+rsi*4], ecx 
     lea  ecx, [rax+2] 

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

Почему существует такая большая разница в сгенерированном коде?

Является ли компилятор GCC C++ лучше вообще при оптимизации кода, который действителен как на C, так и на C++ по сравнению с C-компилятором?

+2

Дополнительная точка данных: использование 'static unsigned int buffer [2048];' делает код C похожим. Вы должны будете фактически использовать «буфер», чтобы он не был полностью устранен.Похоже, что это проблема выравнивания, дополнительный код предназначен для устранения несоосности. – Jester

+1

Идентичный синтаксис/грамматика не подразумевает идентичную семантику. Почему вы ожидаете, что ** разные ** языки C и C++ должны генерировать один и тот же код? Этот вопрос лучше задавать на форуме gcc. – Olaf

+9

@ Олаф, возможно, вы можете наполнить нас различием между семантикой в ​​C и C++ для этой части кода –

ответ

39

Дополнительный код предназначен для обработки несоосности, поскольку используемая инструкция, vmovdqa64, требует 64 байт.

Мое тестирование показывает, что, несмотря на то, что стандарт не работает, gcc разрешает определение в другом модуле переопределять значение здесь, когда он находится в режиме C. Это определение может соответствовать только основным требованиям к выравниванию (4 байта), поэтому компилятор не может полагаться на большее выравнивание. Технически gcc испускает директиву сборки .comm для этого предварительного определения, в то время как внешнее определение использует обычный символ в разделе .data. При связывании этот символ имеет приоритет над .comm.

Обратите внимание, что если вы измените программу на использование extern unsigned int buffer[2048];, тогда даже версия C++ будет иметь добавленный код. И наоборот, делая это static unsigned int buffer[2048]; превратит версию C в оптимизированную.

+0

Нет никакой разницы между C и C++ относительно связи в этом коде. В обоих языках: (a) 'buffer' имеет внешнюю привязку, (b) никакая другая единица не может * определить *' buffer', (c) другие единицы могут * объявить * 'buffer'. –

+0

«В случае C, массив может иметь определение в другом модуле» - это было бы неопределенным поведением в стандарте C. Обсуждение о связи выглядит как красная селедка, учитывая, что добавление '= {0}' также создает " оптимизированная "версия –

+2

Примечание. Для компиляции версии кода C с помощью gcc вы можете добавить флаг компилятора' -fno-common' или аннотировать переменную 'buffer' с помощью' __attribute __ ((aligned (64))), и это будет генерировать аналогичный код для версии C++. – nos