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

Большие различия в генерации кода GCC при компиляции как С++ vs C

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

Чтобы увидеть, как различные 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, я был очень удивлен.

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

Ниже показан фрагмент сгенерированного кода, строки от 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-компилятором?

4b9b3361

Ответ 1

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

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

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