Как убедить avr-gcc, что позиция памяти глобального байтового массива является константой

Я написал быструю процедуру "8-битный реверс" для проекта avr с процессором ATmega2560. я использую

  • GNU C ( WinAVR 20100110) версия 4.3.3 (avr) / скомпилирована GNU C версии 3.4.5 (mingw-vista special r3), GMP версия 4.2.3, MPFR версия 2.4.1.

Сначала я создал глобальную таблицу поиска обращенных байтов (размер: 0x100):

uint8_t BitReverseTable[]
        __attribute__((__progmem__, aligned(0x100))) = {
    0x00,0x80,0x40,0xC0,0x20,0xA0,0x60,0xE0,
    0x10,0x90,0x50,0xD0,0x30,0xB0,0x70,0xF0,
    [...]
    0x1F,0x9F,0x5F,0xDF,0x3F,0xBF,0x7F,0xFF
};

Это работает как ожидалось. Вот макрос, который я собираюсь использовать, который должен стоить мне всего 5 цилиндров:

#define BITREVERSE(x) (__extension__({                                        \
    register uint8_t b=(uint8_t)x;                                            \
    __asm__ __volatile__ (                                                    \
        "ldi r31, hi8(table)"                                          "\n\t" \
        "mov r30, ioRegister"                                          "\n\t" \
        "lpm ioRegister, z"                                            "\n\t" \
        :[ioRegister] "+r" (b)                                                \
        :[table] "g" (BitReverseTable)                                        \
        :"r30", "r31"                                                         \
    );                                                                        \
}))

Код для его компиляции (или нет).

int main() /// Test for bitreverse
{
    BITREVERSE(25);
    return 0;
}

Это ошибка, которую я получаю от компилятора:

c:/winavr-20100110/bin/../lib/gcc/avr/4.3.3/../../../../avr/bin/as.exe -mmcu=atmega2560 -o bitreverse.o C:\Users\xxx\AppData\Local\Temp/ccCefE75.s
C:\Users\xxx\AppData\Local\Temp/ccCefE75.s: Assembler messages:
C:\Users\xxx\AppData\Local\Temp/ccCefE75.s:349: Error: constant value required
C:\Users\xxx\AppData\Local\Temp/ccCefE75.s:350: Error: constant value required

Я думаю, что проблема здесь:

    :[table] "g" (BitReverseTable)                                        \

С моей точки зрения, BitReverseTable - это позиция в памяти массива, которая фиксирована и известна во время компиляции. Поэтому оно постоянно. Может быть, мне нужно преобразовать BitReverseTable во что-то (я пробовал все, что мог придумать). Может быть, мне нужно другое ограничение ("g" был моим последним тестом). Я уверен, что использовал все возможное и невозможное. Я кодировал версию на ассемблере, которая отлично работает, но вместо встроенного кода на ассемблере это правильная функция, которая добавляет еще 6 циклов (для call и ret).

Любые советы или предложения приветствуются!

Полный источник bitreverse.c на pastebin. Подробный вывод компилятора также на pastebin

1 ответ

Решение

Следующее, похоже, работает на avr-gcc (GCC) 4.8.2, но у него есть отчетливое хакерское послевкусие для меня.

Отредактировано, чтобы исправить проблемы, указанные ОП (Томас) в комментариях:

  • Старший байт Z зарегистрироваться r31 (Я имел r30 а также r31 поменять местами)
  • Более новые AVR как ATmega2560 также поддерживают lpm r,Z (только старые AVR lpm r0,Z)

Спасибо за исправления, Томас! У меня есть плата ATmega2560, но я предпочитаю Teensies (частично из-за родного USB), поэтому я только тестировал код, но не запускал его для проверки. Я должен был упомянуть это; Извинения.

const unsigned char reverse_bits_table[256] __attribute__((progmem, aligned (256))) = {
      0, 128,  64, 192,  32, 160,  96, 224,  16, 144,  80, 208,  48, 176, 112, 240,
      8, 136,  72, 200,  40, 168, 104, 232,  24, 152,  88, 216,  56, 184, 120, 248,
      4, 132,  68, 196,  36, 164, 100, 228,  20, 148,  84, 212,  52, 180, 116, 244,
     12, 140,  76, 204,  44, 172, 108, 236,  28, 156,  92, 220,  60, 188, 124, 252,
      2, 130,  66, 194,  34, 162,  98, 226,  18, 146,  82, 210,  50, 178, 114, 242,
     10, 138,  74, 202,  42, 170, 106, 234,  26, 154,  90, 218,  58, 186, 122, 250,
      6, 134,  70, 198,  38, 166, 102, 230,  22, 150,  86, 214,  54, 182, 118, 246,
     14, 142,  78, 206,  46, 174, 110, 238,  30, 158,  94, 222,  62, 190, 126, 254,
      1, 129,  65, 193,  33, 161,  97, 225,  17, 145,  81, 209,  49, 177, 113, 241,
      9, 137,  73, 201,  41, 169, 105, 233,  25, 153,  89, 217,  57, 185, 121, 249,
      5, 133,  69, 197,  37, 165, 101, 229,  21, 149,  85, 213,  53, 181, 117, 245,
     13, 141,  77, 205,  45, 173, 109, 237,  29, 157,  93, 221,  61, 189, 125, 253,
      3, 131,  67, 195,  35, 163,  99, 227,  19, 147,  83, 211,  51, 179, 115, 243,
     11, 139,  75, 203,  43, 171, 107, 235,  27, 155,  91, 219,  59, 187, 123, 251,
      7, 135,  71, 199,  39, 167, 103, 231,  23, 151,  87, 215,  55, 183, 119, 247,
     15, 143,  79, 207,  47, 175, 111, 239,  31, 159,  95, 223,  63, 191, 127, 255,
};

#define USING_REVERSE_BITS \
    register unsigned char r31 asm("r31"); \
    asm volatile ( "ldi r31,hi8(reverse_bits_table)\n\t" : [r31] "=d" (r31) )

#define REVERSE_BITS(v) \
    ({ register unsigned char r30 asm("r30") = v; \
       register unsigned char ret; \
       asm volatile ( "lpm %[ret],Z\n\t" : [ret] "=r" (ret) : [r30] "d" (r30), [r31] "d" (r31) ); \
       ret; })

unsigned char reverse_bits(const unsigned char value)
{
    USING_REVERSE_BITS;
    return REVERSE_BITS(value);
}

void reverse_bits_in(unsigned char *string, unsigned char length)
{
    USING_REVERSE_BITS;

    while (length-->0) {
        *string = REVERSE_BITS(*string);
        string++;
    }
}

Для старых AVR, которые поддерживают только lpm r0,Zиспользовать

#define REVERSE_BITS(v) \
    ({ register unsigned char r30 asm("r30") = v; \
       register unsigned char ret asm("r0"); \
       asm volatile ( "lpm %[ret],Z\n\t" : [ret] "=t" (ret) : [r30] "d" (r30), [r31] "d" (r31) ); \
       ret; })

Идея в том, что мы используем локальный регистр r31, чтобы сохранить старший байт Z зарегистрировать пару. USING_REVERSE_BITS; макрос определяет его в текущей области, используя встроенную сборку для двух целей: избежать ненужной загрузки нижней части адреса таблицы в регистр и убедиться, что GCC знает, что мы сохранили в нем значение (потому что это операнд вывода), не имея возможности узнать, каким должно быть значение, и, следовательно, мы надеемся сохранить его во всей области видимости.

REVERSE_BITS() макрос возвращает результат, сообщая компилятору, что ему нужен аргумент в регистре r30и старший адрес таблицы, установленный USING_REVERSE_BITS; в r31,

Звучит немного сложно, но это только потому, что я не знаю, как объяснить это лучше. Это действительно довольно просто.

Компиляция выше с avr-gcc-4.8.2 -O2 -fomit-frame-pointer -mmcu=atmega2560 -S дает источник сборки. (Я рекомендую использовать -O2 -fomit-frame-pointer.) Пропуск комментариев и нормальных директив:

    .text

reverse_bits:
    ldi r31,hi8(reverse_bits_table)
    mov r30,r24
    lpm r24,Z
    ret

reverse_bits_in:
    mov r26,r24
    mov r27,r25
    ldi r31,hi8(reverse_bits_table)
    ldi r24,lo8(-1)
    add r24,r22
    tst r22
    breq .L2
.L8:
    ld r30,X
    lpm r30,Z
    st X+,r30
    subi r24,1
    brcc .L8
.L2:
    ret

    .section    .progmem.data,"a",@progbits
    .p2align    8
reverse_bits_table:
    .byte    0
    .byte    -128
    ; Rest of data omitted for brevity

Если вам интересно, на ATmega2560 GCC помещает первый 8-битный параметр и результат 8-битной функции в регистр r24,

Насколько я могу судить, первая функция оптимальна. (На старых AVR, которые поддерживают только lpm r0,Z, вы получаете дополнительный ход, чтобы скопировать результат из r0 в r24.)

Для второй функции часть настройки может быть не совсем оптимальной (например, вы можете сделать tst r22breq .L2 первое, что ускорит проверку массива нулевой длины), но я не уверен, что смогу написать быстрее / короче сам; это конечно приемлемо для меня.

Цикл во второй функции выглядит для меня оптимальным. Как это использует r30 Сначала я находил странным и страшным, но потом понял, что в этом есть смысл - меньше регистров используется, и повторное использование не повредит r30 таким образом (даже если это низкая часть Z зарегистрироваться тоже), потому что он будет загружен с новым значением из string в начале следующей итерации.

Обратите внимание, что в моем предыдущем редактировании я упоминал, что изменение порядка параметров функции дает лучший код, но с добавлением Томаса это уже не так. Регистры меняются, вот и все.

Если вы уверены, что всегда поставляете больше нуля length, с помощью

void reverse_bits_in(unsigned char *string, unsigned char length)
{
    USING_REVERSE_BITS;
    do {
        *string = REVERSE_BITS(*string);
        string++;
    } while (--length);
}

доходность

reverse_bits_in:
    mov r26,r24                      ; 1 cycle
    mov r27,r25                      ; 1 cycle
    ldi r31,hi8(reverse_bits_table)  ; 2 cycles
.L4:
    ld r30,X                         ; 2 cycles
    lpm r30,Z                        ; 3 cycles
    st X+,r30                        ; 2 cycles
    subi r22,lo8(-(-1))              ; 1 cycle
    brne .L4                         ; 2 cycles
    ret                              ; 4 cycles

который начинает казаться совершенно впечатляющим для меня: десять циклов на байт, четыре цикла для настройки и три цикла очистки (brne занимает всего один цикл, если не прыгать). Количество циклов, которое я перечислил в верхней части моей головы, так что, вероятно, в них есть небольшие ошибки (цикл здесь или там). r26:r27 является Xи первый параметр указателя на функцию передается в r24:r25с длиной в r22,

reverse_bits_table находится в правильном разделе и правильно выровнен. (.p2align 8 выравнивает до 256 байт; это определяет выравнивание, где младшие 8 битов равны нулю.)

Хотя GCC печально известен избыточными перемещениями регистров, мне действительно нравится код, который он генерирует выше. Конечно, всегда есть место для ловли; для важных последовательностей кода я рекомендую попробовать разные варианты, даже изменить порядок параметров функции (или объявить переменные цикла в локальных областях) и т. д., а затем скомпилировать, используя -S чтобы увидеть сгенерированный код. Синхронизация команд AVR проста, поэтому довольно легко сравнивать последовательности кода, чтобы увидеть, является ли он явно лучше. Я хотел бы сначала удалить директивы и комментарии; это облегчает чтение сборки.


Причиной хакерского послевкусия является то, что в документации GCC явно сказано, что "определение такой переменной регистра не резервирует регистр; оно остается доступным для других целей в тех местах, где управление потоком определяет, что значение переменной не является действующим", и я просто Не верьте, что это означает для разработчиков GCC то же самое, что и для меня. Даже если бы это произошло прямо сейчас, это может произойти не в будущем; здесь не следует придерживаться стандартных разработчиков GCC, поскольку это особенность GCC.

С другой стороны, я полагаюсь только на документированное поведение GCC, описанное выше, и, хотя и "хакерский", он генерирует эффективную сборку из простого C-кода.

Лично я бы рекомендовал перекомпилировать приведенный выше тестовый код и смотреть на сгенерированную сборку (возможно, использовать sed, чтобы удалить комментарии и метки и сравнить с известной хорошей версией?), Всякий раз, когда вы обновляете avr-gcc.

Вопросы?

Другие вопросы по тегам