Как работает mtune?

Вот этот смежный вопрос: GCC: как отличается марш от mtune?

Однако существующие ответы не идут намного дальше, чем руководство GCC. В лучшем случае получим:

Если вы используете -mtune, тогда компилятор будет генерировать код, который работает на любой из них, но будет способствовать последовательности команд, которые работают быстрее всего указанный вами CPU.

и

Опция -mtune=Y настраивает сгенерированный код для ускорения на Y, чем на других процессорах он может работать.

Но как именно GCC предпочитает одну конкретную архитектуру при запуске, все еще будучи способным запускать сборку на других (обычно более старых) архитектурах, хотя и медленнее?

Я знаю только одно (но я не компьютерный ученый), который был бы способен на такое, и что диспетчер CPU. Тем не менее, мне кажется, что mtune генерирует диспетчер за кулисами, и, вероятно, действует какой-то другой механизм.

Я чувствую это по двум причинам:

  • Поиск "gcc mtune cpu dispatcher" не находит ничего актуального; и
  • Если он был основан на диспетчере, я думаю, что он может быть более умным (даже если по какой-либо опции, кроме mtune), и проверить cpuid на обнаружение поддерживаемых инструкций во время выполнения, вместо того, чтобы полагаться на именованную архитектуру, которая предоставляемые во время сборки.

Итак, как это работает?

Ответ 1

-mtune не создает диспетчера, он не нужен: мы уже сообщаем компилятору, в какой архитектуре мы нацеливаемся.

Из Документы GCC:

-mtune = cpu-type

                      набор доступных инструкций.

Это означает, что GCC не будет использовать инструкции, доступные только для cpu-type 1 но он будет генерировать код, который выполняется оптимально для cpu-типа.

Чтобы понять это последнее утверждение, необходимо понять разницу между архитектурой и микроархитектурой.
Архитектура подразумевает ISA (архитектура набора инструкций) и не влияет на -mtune.
Микроархитектура - это то, как архитектура реализована в аппаратном обеспечении. Для равного набора команд (чтение: архитектура) последовательность кода может выполняться оптимально на процессоре (читая микро-архитектуру), но не на другом, из-за внутренних деталей реализации. Это может привести к тому, что последовательность кода будет оптимальной только на одной микроархитектуре.

При генерации машинного кода часто GCC имеет определенную степень свободы в выборе того, как заказывать инструкции и какой вариант использовать.
Он будет использовать эвристику для генерации последовательности инструкций, которые быстро работают на наиболее распространенных процессорах, когда-нибудь она пожертвует 100% -ным оптимальным решением для CPU x, если это будет наказывать процессоры y, z и w.

Когда мы используем -mtune=x, мы точно настраиваем вывод GCC для CPU x, создавая таким образом код, который на 100% оптимален (с точки зрения GCC) на этом CPU.

В качестве конкретного примера рассмотрим как скомпилирован этот код:

float bar(float a[4], float b[4])
{
    for (int i = 0; i < 4; i++)
    {
        a[i] += b[i];
    }

    float r=0;

    for (int i = 0; i < 4; i++)
    {
        r += a[i];
    }

    return r;
} 

a[i] += b[i]; векторизован (если векторы не перекрываются) по-разному при ориентации на Skylake или Core2:

Skylake

    movups  xmm0, XMMWORD PTR [rsi]
    movups  xmm2, XMMWORD PTR [rdi]
    addps   xmm0, xmm2
    movups  XMMWORD PTR [rdi], xmm0
    movss   xmm0, DWORD PTR [rdi] 

Core2

    pxor    xmm0, xmm0
    pxor    xmm1, xmm1
    movlps  xmm0, QWORD PTR [rdi]
    movlps  xmm1, QWORD PTR [rsi]
    movhps  xmm1, QWORD PTR [rsi+8]
    movhps  xmm0, QWORD PTR [rdi+8]
    addps   xmm0, xmm1
    movlps  QWORD PTR [rdi], xmm0
    movhps  QWORD PTR [rdi+8], xmm0
    movss   xmm0, DWORD PTR [rdi]

Основное отличие заключается в том, как загружается регистр xmm, на Core2 он загружается двумя нагрузками, используя movlps и movhps вместо одного movups.
Оба подхода с двумя нагрузками лучше подходят для микроархитектуры Core2, если вы посмотрите на таблицы инструкций Agner Fog, вы увидите, что movups декодируется в 4 раза и имеет задержку в 2 цикла, тогда как каждый movXps равен 1 uop и 1 цикл латентности.
Вероятно, это связано с тем, что 128-битные обращения были разделены на два 64-разрядных доступа в то время.
На Skylake верно обратное: movups выполняет лучше двух movXps.

Итак, мы должны подобрать один.
В общем, GCC выбирает первый вариант, потому что Core2 - это старая микроархитектура, но мы можем переопределить это с помощью -mtune.


1 Набор команд выбирается другими клавишами.