Как убедить 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
(только старые AVRlpm 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 r22
breq .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.
Вопросы?