Почему код сдвига вправо в g cc отличается в режиме C и C ++? - PullRequest
10 голосов
/ 19 июня 2020

Когда ARM g cc 9.2.1 заданы параметры командной строки -O3 -xc++ -mcpu=cortex-m0 [компилировать как C ++] и следующий код:

unsigned short adjust(unsigned short *p)
{
    unsigned short temp = *p;
    temp -= temp>>15;
    return temp;
}

, получается разумный машинный код:

    ldrh    r0, [r0]
    lsrs    r3, r0, #15
    subs    r0, r0, r3
    uxth    r0, r0
    bx      lr

, что эквивалентно:

unsigned short adjust(unsigned short *p)
{
    unsigned r0,r3;
    r0 = *p;
    r3 = temp >> 15;
    r0 -= r3;
    r0 &= 0xFFFFu;   // Returning an unsigned short requires...
    return r0;       //  computing a 32-bit unsigned value 0-65535.
}

Очень разумно. Последний "uxtw" можно было бы опустить в данном конкретном случае, но для компилятора, который не может доказать безопасность такой оптимизации, лучше проявить осторожность, чем рисковать возвратом значения за пределами диапазона 0-65535, что может полностью поглотить нисходящий код.

При использовании -O3 -xc -mcpu=cortex-m0 [идентичные параметры, за исключением компиляции как C, а не C ++], однако код меняется:

    ldrh    r3, [r0]
    movs    r2, #0
    ldrsh   r0, [r0, r2]
    asrs    r0, r0, #15
    adds    r0, r0, r3
    uxth    r0, r0
    bx      lr

unsigned short adjust(unsigned short *p)
{
    unsigned r0,r2,r3;
    r3 = *p;
    r2 = 0;
    r0 = ((unsigned short*)p)[r2];
    r0 = ((int)r0) >> 15;  // Effectively computes -((*p)>>15) with redundant load
    r0 += r3
    r0 &= 0xFFFFu;     // Returning an unsigned short requires...
    return temp;       //  computing a 32-bit unsigned value 0-65535.
}

Я знаю, что определенные угловые случаи для сдвига влево отличаются в C и C ++, но я думал, что сдвиги вправо одинаковы. Есть ли что-то иное в том, как работают сдвиги вправо в C и C ++, что заставило бы компилятор использовать другой код для их обработки? Версии до 9.2.1 генерируют немного меньше плохого кода в режиме C:

    ldrh    r3, [r0]
    sxth    r0, r3
    asrs    r0, r0, #15
    adds    r0, r0, r3
    uxth    r0, r0
    bx      lr

эквивалентно:

unsigned short adjust(unsigned short *p)
{
    unsigned r0,r3;
    r3 = *p;
    r0 = (short)r3;
    r0 = ((int)r0) >> 15; // Effectively computes -(temp>>15)
    r0 += r3
    r0 &= 0xFFFFu;     // Returning an unsigned short requires...
    return temp;       //  computing a 32-bit unsigned value 0-65535.
}

Не так плохо, как версия 9.2.1, но все же инструкция длиннее, чем могла бы быть прямая трансляция кода. При использовании 9.2.1 объявление аргумента как unsigned short volatile *p устранит избыточную нагрузку p, но мне любопытно, почему g cc 9.2.1 потребуется квалификатор volatile, чтобы избежать избыточной нагрузки , или почему такая причудливая «оптимизация» происходит только в режиме C, а не в режиме C ++. Мне также несколько любопытно, почему g cc даже подумал бы о добавлении ((short)temp) >> 15 вместо вычитания temp >> 15. Есть ли какой-то этап оптимизации, на котором это могло бы иметь смысл?

1 Ответ

3 голосов
/ 20 июня 2020

Разница, по-видимому, связана с разницей в интегральном продвижении temp между режимами компиляции G CC C и C ++.

Использование «Tree / RTL Viewer» в компиляторе Explorer, можно заметить, что когда код компилируется как C ++, G CC повышает temp до int для операции сдвига вправо. Однако при компиляции как C temp повышается только до signed short ( на godbolt ):

G CC дерево с -xc++:

{
  short unsigned int temp = *p;

  # DEBUG BEGIN STMT;
    short unsigned int temp = *p;
  # DEBUG BEGIN STMT;
  <<cleanup_point <<< Unknown tree: expr_stmt
  (void) (temp = temp - (short unsigned int) ((int) temp >> 15)) >>>>>;
  # DEBUG BEGIN STMT;
  return <retval> = temp;
}

с -xc:

{
  short unsigned int temp = *p;

  # DEBUG BEGIN STMT;
    short unsigned int temp = *p;
  # DEBUG BEGIN STMT;
  temp = (short unsigned int) ((signed short) temp >> 15) + temp;
  # DEBUG BEGIN STMT;
  return temp;
}

Приведение к signed short делается явным только при сдвиге temp на один бит меньше его 16-битного размера; при сдвиге менее чем на 15 бит преобразование исчезает, и код компилируется в соответствии с "разумными" инструкциями -xc++. Неожиданное поведение также происходит при использовании unsigned char s и сдвиге на 7 бит.

Интересно, что armv7-a clang не дает такого же поведения; и -xc, и -xc++ дают "разумный" результат:

    ldrh    r0, [r0]
    sxth    r0, r0
    lsrs    r1, r0, #15
    adds    r0, r1, r0
    uxth    r0, r0
    bx      lr

Обновление: Похоже, эта «оптимизация» связана либо с буквальным 15, либо с использованием вычитания (или унарного -) со сдвигом вправо:

  • Размещение литерала 15 в переменной unsigned short приводит к тому, что как -xc, так и -xc++ генерируют разумные инструкции.
  • Замена temp>>15 на temp/(1<<15) также вызывает оба варианта для создания разумных инструкций.
  • Изменение сдвига на temp>>(-65521) приводит к тому, что обе опции производят более длинную арифметическую версию c -сдвига, при этом -xc++ также приводит к преобразованию temp в signed short в пределах сдвига.
  • Удаление негатива из операции сдвига (temp = -temp + temp>>15; return -temp;) заставляет оба варианта давать разумные инструкции.

См. Примеры на Godbolt . Я согласен с @supercat, что это может быть просто странным случаем правила как если бы . Выводы, которые я вижу из этого, заключаются в том, чтобы либо избежать вычитания без знака с неконстантными, либо за это сообщение SO о продвижении int, возможно, не пытайтесь заставить арифметику c меньше, чем - int типы хранения.

...