Почему ARM gcc вызывает __udivsi3 при делении на константу?

Я использую последнюю доступную версию GCC с пакетом ARM:

arm-none-eabi-gcc (GNU Arm Embedded Toolchain 10-2020-q4-major) 10.2.1 20201103 (выпуск) Copyright (C) 2020 Free Software Foundation, Inc.

Когда я компилирую этот код с помощью -mcpu = cortex-m0 -mthumb -Ofast:

int main(void) {
    uint16_t num = (uint16_t) ADC1->DR;
    ADC1->DR = num / 7;
}

Я ожидал, что деление будет выполнено умножением и сдвигом, но вместо этого генерируется этот код:

 08000b5c <main>:
 8000b5c: b510 push {r4, lr}
 8000b5e: 4c05 ldr r4, [pc, #20] ; (8000b74 <main+0x18>)
 8000b60: 2107 movs r1, #7
 8000b62: 6c20 ldr r0, [r4, #64] ; 0x40
 8000b64: b280 uxth r0, r0
 8000b66: f7ff facf bl 8000108 <__udivsi3>
 8000b6a: b280 uxth r0, r0
 8000b6c: 6420 str r0, [r4, #64] ; 0x40
 8000b6e: 2000 movs r0, #0
 8000b70: bd10 pop {r4, pc}
 8000b72: 46c0 nop ; (mov r8, r8)
 8000b74: 40012400 .word 0x40012400

Использование __udivsi3 вместо умножения и сдвига ужасно неэффективно. Я использую неправильные флаги или что-то упускаю, или это ошибка GCC?


person Dan Sandberg    schedule 22.03.2021    source источник


Ответы (3)


В Cortex-M0 отсутствуют инструкции для выполнения 32x32- ›64-битного умножения. Поскольку num является беззнаковой 16-битной величиной, умножение его на 9363 и сдвиг вправо на 16 даст правильный результат во всех случаях, но - вероятно, поскольку uint16_t будет повышен до int перед умножением, gcc не включает такие оптимизации.

Из того, что я наблюдал, gcc, как правило, плохо справляется с оптимизацией для Cortex-M0, не используя некоторые простые оптимизации, которые были бы подходящими для этой платформы, но иногда использует оптимизацию, которая не подходит. Учитывая что-то вроде

void test1(uint8_t *p)
{
    for (int i=0; i<32; i++)
        p[i] = (p[i]*9363) >> 16; // Divide by 7
}

gcc генерирует нормальный код для Cortex-M0 в -O2, но если бы умножение было заменено добавлением, компилятор сгенерировал бы код, который перезагружает константу 9363 на каждой итерации цикла. При использовании сложения, даже если код был изменен на:

void test2(uint16_t *p)
{
    register unsigned u9363 = 9363;
    for (int i=0; i<32; i++)
        p[i] = (p[i]+u9363) >> 16;
}

gcc по-прежнему будет загружать константу в цикл. Иногда оптимизация gcc также может иметь неожиданные поведенческие последствия. Например, можно было ожидать, что на такой платформе, как Cortex-M0, будет вызвано что-то вроде:

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

в то время как прерывание изменяет содержимое *p, может привести к поведению, согласованному со старым или новым значением. Стандарт не требует такой обработки, но большинство реализаций, предназначенных для задач встроенного программирования, будут предлагать более строгие гарантии, чем то, что требует Стандарт. Если старое или новое значение будут одинаково приемлемыми, разрешение компилятору использовать то, что более удобно, может позволить получить более эффективный код, чем использование volatile. Однако, как это случилось, оптимизированный код из gcc заменит два использования temp отдельными загрузками *p.

Если вы используете gcc с Cortex-M0 и вас не беспокоит производительность или возможность удивительного поведения, возьмите за привычку проверять вывод компилятора. Для некоторых видов циклов, возможно, стоит подумать о тестировании -O0. Если в коде надлежащим образом используется ключевое слово register, его производительность иногда может превзойти производительность идентичного кода, обработанного с помощью -O2.

person supercat    schedule 22.03.2021
comment
Отличный ответ, спасибо. Есть ли компилятор, который вы могли бы предложить, который, кажется, правильно раскрывает детали для Cortex-M0? - person Dan Sandberg; 23.03.2021
comment
@DanSandberg: Я использую ARM-Keil MDK на работе. Это довольно дорого, но вы можете скачать бесплатную ознакомительную версию с ограниченной функциональностью. - person supercat; 23.03.2021
comment
Если эта проблема затрагивает вас, пожалуйста, отметьте ошибку как затрагивающую вас здесь: bugs. launchpad.net/gcc-arm-embedded/+bug/1920818 - person Dan Sandberg; 23.03.2021
comment
@DanSandberg: Эта проблема разделения является верхушкой айсберга в том, что касается низкой эффективности генерации кода Cortex-M0. Вы смотрели код, который gcc генерирует для последней test функции выше? Если или до тех пор, пока разработчики gcc не будут заинтересованы в попытках максимизировать диапазон непереносимых, но полезных конструкций, они могут надежно и эффективно обрабатывать с той же семантикой, что и -O0, без создания ненужных странных угловых случаев как и в test, я бы считал gcc надежным только тогда, когда он использует -O0 для частей кода, которые не проверяются вручную после каждой сборки. - person supercat; 23.03.2021
comment
@DanSandberg: Между прочим, при вычислении x*9363 gcc использует загрузку константы, за которой следует умножение, но при использовании x*37449 он предпочитает генерировать последовательность из восьми инструкций перемещений, сложений и сдвигов. Может быть, хороший компромисс для ARM с медленным умножением, но не с быстрым умножением. - person supercat; 23.03.2021

Расширение ответа суперкота.

Накормите это:

unsigned short fun ( unsigned short x )
{
    return(x/7);
}

к чему-то с большим умножением:

00000000 <fun>:
   0:   e59f1010    ldr r1, [pc, #16]   ; 18 <fun+0x18>
   4:   e0832190    umull   r2, r3, r0, r1
   8:   e0400003    sub r0, r0, r3
   c:   e08300a0    add r0, r3, r0, lsr #1
  10:   e1a00120    lsr r0, r0, #2
  14:   e12fff1e    bx  lr
  18:   24924925    .word   0x24924925
  

1/7 в двоичном формате (длинное деление):

     0.001001001001001
 111)1.000000
       111 
      ==== 
         1000
          111
          ===
            1
            
        
0.001001001001001001001001001001
0.0010 0100 1001 0010 0100 1001 001001
0x2492492492...
0x24924925>>32  (rounded up)

Чтобы это работало, вам нужен 64-битный результат, вы берете верхнюю половину и вносите некоторые изменения, например:

7 * 0x24924925 = 0x100000003

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

Умножение варианта all thumbs составляет 32 бита = 32 бита * 32 бита, поэтому результат будет 0x00000003, и это не сработает.

Итак, 0x24924, который мы можем сделать 0x2493, как это сделал supercat, или 0x2492.

Теперь мы можем использовать умножение 32x32 = 32 бит:

0x2492 * 7 = 0x0FFFE
0x2493 * 7 = 0x10005

Давайте пробежимся с тем, что побольше:

0x100000000/0x2493 = a number greater than 65536. so that is fine.

но:

0x3335 * 0x2493 = 0x0750DB6F
0x3336 * 0x2493 = 0x07510002
0x3335 / 7 = 0x750
0x3336 / 7 = 0x750

Так что вы можете продвинуться только с таким подходом.

Если следовать модели кода руки:

for(ra=0;ra<0x10000;ra++)
{
    rb=0x2493*ra;
    rd=rb>>16;
    rb=ra-rd;
    rb=rd+(rb>>1);
    rb>>=2;
    rc=ra/7;
    printf("0x%X 0x%X 0x%X \n",ra,rb,rc);
    if(rb!=rc) break;
}

Затем он работает от 0x0000 до 0xFFFF, поэтому вы можете написать asm для этого (обратите внимание, что это должно быть 0x2493, а не 0x2492).

Если вы знаете, что операнд не превышает определенного значения, вы можете использовать больше битов 1/7 для умножения.

В любом случае, когда компилятор не выполняет эту оптимизацию за вас, у вас все еще есть шанс.

Теперь, когда я думаю об этом, я сталкивался с этим раньше, и теперь это имеет смысл. Но у меня была полноразмерная рука, и я вызвал подпрограмму, которую я скомпилировал в режиме руки (другой код был в режиме большого пальца), и имел оператор switch в основном, если знаменатель = 1, то результат = x / 1; если знаменатель = 2, то результат = x / 2 и так далее. А затем он отказался от функции gcclib и сгенерировал умножение 1 / x. (У меня было 3 или 4 разных константы для деления):

unsigned short udiv7 ( unsigned short x )
{
    unsigned int r0;
    unsigned int r3;
    
    r0=x;
    r3=0x2493*r0;
    r3>>=16;
    r0=r0-r3;
    r0=r3+(r0>>1);
    r0>>=2;
    return(r0);
}

Предполагая, что я не сделал ошибок:

00000000 <udiv7>:
   0:   4b04        ldr r3, [pc, #16]   ; (14 <udiv7+0x14>)
   2:   4343        muls    r3, r0
   4:   0c1b        lsrs    r3, r3, #16
   6:   1ac0        subs    r0, r0, r3
   8:   0840        lsrs    r0, r0, #1
   a:   18c0        adds    r0, r0, r3
   c:   0883        lsrs    r3, r0, #2
   e:   b298        uxth    r0, r3
  10:   4770        bx  lr
  12:   46c0        nop         ; (mov r8, r8)
  14:   00002493    .word   0x00002493

Это должно быть быстрее, чем обычная процедура библиотеки деления.

Редактировать

Думаю, я вижу, что Supercat сделал с работающим решением:

((i*37449 + 16384u) >> 18)

У нас есть это как 1/7 дробь:

0.001001001001001001001001001001

но мы можем сделать только умножение 32 = 32x32 бит. Начальные нули дают нам некоторую передышку, которой мы могли бы воспользоваться. Поэтому вместо 0x2492 / 0x2493 мы можем попробовать:

1001001001001001
0x9249
0x9249*0xFFFF = 0x92486db7

И пока не переполнится:

    rb=((ra*0x9249) >> 18);

сам по себе он не работает при 7 * 0x9249 = 0x3FFFF, 0x3FFFF ›› 18 равно нулю, а не 1.

Так что, может быть

    rb=((ra*0x924A) >> 18);

что не удается:

    0xAAAD 0x1862 0x1861 

Так что насчет:

    rb=((ra*0x9249 + 0x8000) >> 18);

и это работает.

А что насчет суперкотов?

    rb=((ra*0x9249 + 0x4000) >> 18);

и это работает чисто для всех значений от 0x0000 до 0xFFFF:

    rb=((ra*0x9249 + 0x2000) >> 18);

и здесь это не работает:

0xE007 0x2000 0x2001 

Итак, есть несколько эффективных решений.

unsigned short udiv7 ( unsigned short x )
{
    unsigned int ret;
    ret=x;
    ret=((ret*0x9249 + 0x4000) >> 18);
    return(ret);
}
00000000 <udiv7>:
   0:   4b03        ldr r3, [pc, #12]   ; (10 <udiv7+0x10>)
   2:   4358        muls    r0, r3
   4:   2380        movs    r3, #128    ; 0x80
   6:   01db        lsls    r3, r3, #7
   8:   469c        mov ip, r3
   a:   4460        add r0, ip
   c:   0c80        lsrs    r0, r0, #18
   e:   4770        bx  lr
  10:   00009249    .word   0x00009249

Редактировать

Что касается вопроса "почему", это не вопрос переполнения стека; если вы хотите узнать, почему gcc этого не делает, спросите авторов этого кода. Все, что мы можем сделать, это предположить здесь, и предположение состоит в том, что они, возможно, решили не делать этого из-за количества инструкций, или они, возможно, решили не делать этого, потому что у них есть алгоритм, который заявляет, что это не 64 = 32x32 битное умножение, тогда выполните не беспокоить.

Опять же, вопрос «почему» не является вопросом переполнения стека, поэтому, возможно, нам следует просто закрыть этот вопрос и удалить все ответы.

Я обнаружил, что это было невероятно познавательным (если вы знаете / понимаете, о чем говорилось).

Другой ПОЧЕМУ? вопрос в том, почему gcc сделал это так, как они это сделали, когда они могли сделать это так же, как supercat или я?

person old_timer    schedule 23.03.2021
comment
Взаимное умножение, учебное пособие homepage.divms.uiowa.edu/~jones/bcd /divide.html - person old_timer; 23.03.2021
comment
gcc четко знает, как это сделать (попробуйте и другие цели), поэтому я не считаю это ошибкой или пропущенной оптимизацией. Я предполагаю, что у них просто есть отказ от этого после определенного количества кода или если все пальцем -variants тогда не пытайтесь выполнить оптимизацию. - person old_timer; 23.03.2021
comment
Этот пост не имеет никакого отношения к вопросу. Вопрос был в том, почему gcc не делает это автоматически? не как мне сделать это самому? что ОП, очевидно, уже знает, иначе они не задали бы вопрос. - person Tom V; 23.03.2021

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

Поскольку 7 является простым числом с 2, невозможно разделить любой ввод на семь с помощью умножения и сдвига.

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

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

В общем, GCC будет выполнять деление с использованием сдвига только в том случае, если делитель не является взаимно простым с 2, то есть если он является степенью двойки.

person Tom V    schedule 22.03.2021
comment
Для входных данных в диапазоне 0-65535 (num - это uint16_t) умножение и сдвиг легко дали бы точные результаты, если бы компилятор был заинтересован в такой оптимизации. - person supercat; 23.03.2021
comment
Да, для uint16_t, деленного на 7, но не для любого произвольного постоянного делителя, и поскольку это 32-битная платформа, делитель обычно также не будет 16-битным. Я пытался подчеркнуть, что это угловой случай, который обычно не применим. Вот почему никто не посчитал целесообразным добавлять его в компилятор, когда в этих крайних случаях люди могут просто сделать это вручную. - person Tom V; 23.03.2021
comment
Добавление поддержки эффективного разделения 16/16 на 32-битной платформе было бы проще и предлагало бы большую ценность для многих встраиваемых приложений, чем многие из более умных вещей, которые делает gcc. - person supercat; 23.03.2021
comment
Обычно вы умножаете на 1/7, поскольку 7 не является степенью двойки. - person old_timer; 23.03.2021
comment
Смотрите мой ответ, вы умножаете на константу 1/7, в этом случае происходит какое-то смещение или другое, которое происходит в зависимости от константы. (первый сдвиг подразумевается, если 32-битное сопротивление, тогда сдвиг вправо на 32 подразумевается путем взятия регистра, содержащего результат умножения). но по сути это умножение со сдвигом для получения результата. и clang, и gcc сгенерируют эти оптимизации для целей с умножением, но без деления, и вы обнаружите, что в некоторых случаях он будет делать это, даже если у цели есть деление, поскольку умножение выполняется быстрее (или может быть в зависимости от реализации) - person old_timer; 23.03.2021
comment
Вы также можете заменить умножение, если у вас его нет, хотя я подозреваю, что компилятор откажется от этого, как это было здесь с большим пальцем, и просто вызовет библиотечную функцию. - person old_timer; 23.03.2021
comment
@old_timer: для дивидендов, которые производятся из значений uint8_t, uint16_t или констант таким способом, который гарантированно дает значение в диапазоне 0-65535, достаточно простой инструкции умножения. Я ожидал, что для больших входов процедура умножения 32x32, вероятно, будет более эффективной, чем процедура деления на многих платформах, хотя некоторые устройства Cortex-M0 имеют вспомогательную схему деления, которая может свести на нет это преимущество. - person supercat; 23.03.2021
comment
Умножение с использованием 32 = 32x32 недостаточно (x * 9363) ›› 16 не работает во многих случаях между 0 и 0xFFFF. Как показано, если это то, что вы имеете в виду. Возможно, несколько лет назад вы должны были увидеть вопрос о том, почему gcc производит умножение, когда указанный isa имеет деление (для руки). Производители микросхем, безусловно, могут добавить разделитель, если захотят. Однако это был хороший / интересный вопрос. - person old_timer; 23.03.2021
comment
Мне пришлось бы искать, но для M0 или, возможно, также других, поставщик микросхем может скомпилировать для быстрого или медленного умножения, можно было бы утверждать, что могут быть или, возможно, есть параметры командной строки для генерации небиблиотечного кода, где это возможно. Это не тот проект, который мне интересно брать на себя, я бы просто сделал то, что было сделано выше (или синтезировал разделение каким-либо другим способом, чтобы избежать разделения). Одним из примеров может быть просто сохранить значение adc с умножением на семь, а остальную часть математики, возможно, разделить позже или не нужно. - person old_timer; 23.03.2021
comment
8-битный операнд, безусловно, умножение и сдвиг работает нормально. - person old_timer; 23.03.2021
comment
@old_timer: Мозг пердит на моем конце. Правильным выражением будет ((i*37449 + 16384u) >> 18) [проверено для всех значений] - person supercat; 23.03.2021
comment
палец вверх, приятно - person old_timer; 23.03.2021
comment
даже лучше, если вы объясните, как вы это придумали ... ах, да ладно, я понял ... я думаю ... не обязательно 16384, хотя ... это втягивает еще одну цифру? - person old_timer; 23.03.2021