Ниже приведен код, реализованный на 8-битном микроконтроллере.
Следующий комментарий был отправлен на другой вопрос :
Поскольку ваш код не использует
i
переменную, почему бы просто неwhile(length--)
использовать первыйfor
цикл?
Он предлагает изменить for(..)
цикл для a while(..)
, будет ли это иметь какое-либо практическое значение для того, насколько оптимизирован код для встроенного микроконтроллера?
uint32_t MyFunction(uint32_t crc, uint8_t *buffer, uint16_t length) {
for (uint16_t i=0; i < length; i++) {
crc = crc ^ *buffer++;
for (uint16_t j=0; j < 8; j++) {
if (crc & 1)
crc = (crc >> 1) ^ 0xEDB88320;
else
crc = crc >> 1;
}
}
return crc;
}
Примечание. Я написал этот вопрос и сам ответил на него после публикации комментария ранее на этой неделе. За этот комментарий проголосовали, но я чувствовал, что должен подкрепить его реальным тестированием. Другие ответы были бы полезны, но это было задумано как общее доказательство точки зрения, а не конкретный вопрос об алгоритме.
Классические три правила оптимизации:
Во-первых, не помешает написать чистый и аккуратный код, но преждевременная оптимизация — пустая трата времени и усилий, которые можно было бы с большей пользой потратить на что-то другое.
Первое, что нужно рассмотреть, это то, что нужно оптимизировать. Должен ли код занимать меньше памяти программы или он должен работать быстрее? Иногда обе эти вещи могут быть достигнуты за счет уменьшения количества инструкций, однако это зависит от базового микроконтроллера и количества циклов, которое занимает каждая инструкция.
Вполне возможно сгенерировать меньший код, который требует больше циклов для выполнения, особенно если задействованы ветки.
Чтобы начать экспериментировать, важно изолировать наименьшую возможную часть кода. Ниже я создал простой тестовый пример, который включает функцию из вопроса и переносим между разными семействами микроконтроллеров:
#include <stdint.h>
uint32_t MyFunction(uint32_t crc, uint8_t *buffer, uint16_t length) {
uint16_t i;
uint16_t j;
for (i = 0; i < length; i++) {
crc = crc ^ *buffer++;
for (j = 0; j < 8; j++) {
if (crc & 1)
crc = (crc >> 1) ^ 0xEDB88320;
else
crc = crc >> 1;
}
}
return crc;
}
int main(int argc, char** argv) {
uint8_t data[] = "ABCDEF";
uint32_t ret = 0;
ret = MyFunction(ret, data, 6);
while(1);
}
Как отмечено в комментариях к другому вопросу , значение переменной i
никогда не используется напрямую, оно только сравнивается с length
. Следовательно, мы могли бы переписать его следующим образом:
uint32_t MyFunction(uint32_t crc, uint8_t *buffer, uint16_t length) {
uint16_t j;
while (length--) {
// .. do work ..
}
return crc;
}
Для сравнения я использовал avr-gcc версии 4.7.2 (для Atmega8) и Microchip XC8 1.21 (для PIC 18F). Для XC8 я включил оптимизацию PRO, для avr-gcc приводились следующие аргументы: avr-gcc -g -c -Os -Wall -mmcu=atmega8 test.c -o test-Os
.
Обратите внимание, что важно проверить, что ret
это не оптимизировано, потому что компилятор считает, что оно не используется. Оба варианта кода C генерируют одну и ту же сборку из avr-gcc. Однако XC8 не замечает неиспользуемую переменную, поэтому код, использующий while(..)
цикл, компилируется на 4 байта меньше с сохранением 2 байтов ОЗУ.
Как отмечено в комментарии к этому вопросу, также было бы более эффективно сделать j
, uint8_t
так как вход никогда не будет иметь ширину более 8 бит. Тестирование на XC8 показало, что внесение этого изменения экономит еще 8 байт программного пространства и 1 байт ОЗУ. Это также уменьшает вывод, сгенерированный с помощью avr-gcc, на 4 байта и один регистровый байт.
В заключение всегда стоит дать компилятору наилучшие шансы сгенерировать хороший код , в данном случае не используя лишние ненужные переменные и используя наименьший возможный класс памяти. Некоторые оптимизирующие компиляторы справятся лучше, чем другие, но оба они работают хорошо, если входной код C был хорошо продуман.
Такого рода вещи могут быть переписаны на многих процессорах в небольшой процедуре на языке ассемблера, которая будет работать намного быстрее, чем любая возможная реализация C. Например, на PIC вычисление CRC может быть записано с использованием 72 инструкций, если _crc находится в выбранном в данный момент банке (код для PIC серии 16F).
movf _crc+1,w
btfss _crc,0
xorlw 0xNN ; Would need to figure out proper value
btfss _crc,1
xorlw 0xNN ; Would need to figure out proper value
.. six more such instruction pairs
movwf btemp+0 ; LSB of return
movf _crc+2,w
btfss _crc,0
xorlw 0xNN ; Would need to figure out proper value
btfss _crc,1
xorlw 0xNN ; Would need to figure out proper value
.. six more such instruction pairs
movwf btemp+1 ; Next byte of return
movf _crc+3,w
.. eight more instruction pairs
movwf btemp+2,w
movlw 0
.. eight more instruction pairs
movwf btemp+3
Не самая компактная штука в мире, но обновляла бы CRC32 для входящего байта за 72 цикла. В качестве альтернативы, если кто-то использовал часть серии 18F и мог сэкономить место для кода, можно было использовать таблицы объемом 1 Кбайт (организованные в виде четырех выровненных по страницам 256-байтовых фрагментов). Тогда код будет примерно таким:
movf _crc,w,b
movwf _TBLPTRL,c
movlw TabUpperAddress
movwf _TBLPTRU,c
movlw TabHighAddress
movwf _TBLPTRH,c
tblrd *
movff _TABLAT,btemp+3
incf _TBLPTRH,c
tblrd *
xorwf _crc+1,w,b
movff _TABLAT,btemp+0
incf _TBLPTRH,c
tblrd *
xorwf _crc+2,w,b
movff _TABLAT,btemp+1
incf _TBLPTRH,c
tblrd *
xorwf _crc+3,w,b
movff _TABLAT,btemp+2
Немного быстрее, но для этого потребуется 1 Кбайт таблиц данных в дополнение к самому коду.
Олин Латроп
Ренан
апалопохапа
Дэйвид
Дэйвид
Дэйвид