Должен ли я реорганизовать свой код C, чтобы оптимизировать его для встроенного микроконтроллера?

Ниже приведен код, реализованный на 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; 
}

Примечание. Я написал этот вопрос и сам ответил на него после публикации комментария ранее на этой неделе. За этот комментарий проголосовали, но я чувствовал, что должен подкрепить его реальным тестированием. Другие ответы были бы полезны, но это было задумано как общее доказательство точки зрения, а не конкретный вопрос об алгоритме.

Если это на 8-битной машине, зачем намеренно заставлять J быть 16-битным целым числом, когда он будет считать только от 0 до 7?
Вы профилировали его, чтобы увидеть, действительно ли вам нужно его оптимизировать?
Микроконтроллеры обычно включают в себя инструкцию, которая уменьшает значение и выполняет переход, если оно равно/отлично от нуля. Увеличение и сравнение с константой или переменной включает как минимум еще одну инструкцию на цикл. Таким образом, в качестве общей хорошей практики с точки зрения времени создавайте циклы, использующие счетчик, который уменьшается до нуля по завершении.
Единственная причина, по которой я написал это, заключается в том, что я дал совет в другой теме, который я хотел доказать, что он был полезен в некоторых ограниченных обстоятельствах.
@OlinLathrop интересный момент, интересно, XC8 или GCC заметили это и оптимизировали. Это должно быть легкой добычей для компилятора.
@OlinLathrop протестирован, и это экономит еще больше места, мой ответ обновлен ниже. Спасибо за указатель.

Ответы (3)

Классические три правила оптимизации:

  1. не.
  2. еще нет.
  3. профиль перед оптимизацией.

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

Подпрограммы буферизации CRC часто могут быть узким местом на 8-битных микропроцессорах, даже если они написаны оптимально . Кроме того, многие 8-битные микропроцессоры, подобные приведенному коду, вероятно, намного медленнее, чем оптимальный код. Для многих целей код C может быть достаточно близок к ассемблеру, чтобы его не стоила дальнейшая оптимизация, но некоторые виды конструкций циклов требуют оптимизации, которую не может сделать ни один из известных мне компиляторов.
По мнению Брика. «[...] преждевременная оптимизация — корень всех зол», — говорит Дональд Кнут.
Я действительно ненавижу, как далеко люди заходят в этом комментарии Кнута. По крайней мере, вы должны быть внимательны при написании кода, чтобы сделать его достаточно эффективным для начала. Люди цитируют Кнута в качестве оправдания своего небрежного кода и тоже оправдываются. Вы должны разработать хорошие методы программирования, которые помогут вам исправить хороший код. Я не говорю разбирать и конвейерно анализировать все ... но, как говорит @supercat, если вы вообще обращаете внимание на свою проблему, вы, вероятно, поймете, сколько работы будет выполнять ваш алгоритм CRC. Во многих ситуациях это будет иметь абсолютное значение.
подпрограмма CRC на 8-битном процессоре может вообще не быть эффективной или быть абсолютно критической. Без дополнительных подробностей это НЕ хороший пример «общего доказательства» ОП за или против его оптимизации.
@darron: ключевое слово — преждевременная оптимизация, особенно в тех случаях, когда уделение слишком большого внимания оптимизации одной части программы может привести к тому, что кто-то пренебрежет более важной частью, которая представляет собой серьезное узкое место (например, я помню, как взял на себя программу, которую кто-то написал для DSP с фиксированной точкой. Все было написано на ассемблере, кроме математики, которая была написана на C с использованием чисел с плавающей запятой, а IIRC требовалось около 60 мс для обработки каждых 25 мс звука). Я думаю, что моя переделка закончилась несколькими сотнями строк ассемблерного кода для обработчиков прерываний, несколькими десятками для математики и...
... почти все остальное на C. Я даже не стал пытаться писать математические подпрограммы на C. Часть ассемблерного кода математической обработки, вероятно, занимала около 5% общего времени процессора, а C был бы по крайней мере в 3-5 раз медленнее. Использование ЦП с математикой с фиксированной запятой в C могло бы быть ниже 100%, но намного проще рассуждать о таймингах при низкой загрузке, чем при высокой.
@supercat заметил, что в правиле RedGrittyBrick не упоминается слово «преждевременный». По крайней мере, это появляется потом... но МНОГИЕ люди просто полностью опускают это слово. Я думаю, что моя главная проблема здесь в том, что я считаю, что эта логика «оптимизируйте только тогда, когда вам нужно» в более общем случае приводит к дрянной архитектуре ... потому что многие проблемы с производительностью связаны с архитектурой, и очень болезненно изменить это позже. Я думаю, что Кнут предполагал базовый уровень компетентности, который, к сожалению, не отражается в реальности в наши дни.
@darron: я интерпретирую заявление Кнута как «Оптимизация до того, как кто-то узнает, где это необходимо, часто приводит к трате времени на оптимизацию вещей, которые не имеют значения, а не вещей [не обязательно только производительности], которые имеют значение . Я не думаю, что профилирование всегда необходимо. ; при выборе между простым алгоритмом O(N^3) или сложным алгоритмом O(NlgN), например, знание того, что N может достичь 1000, было бы довольно сильным аргументом против первого даже без профилирования, и знание того, что N редко превышать 3 и никогда не превышать 5 будет сильным аргументом против последнего.
@supercat Ну, я бы хотел, чтобы он сказал это по-твоему. Весь ваш абзац, на самом деле. Ты говоришь это лучше, чем я. :)
Инженер, который постоянно превышает спецификации, тратит время и деньги впустую

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

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

Чтобы начать экспериментировать, важно изолировать наименьшую возможную часть кода. Ниже я создал простой тестовый пример, который включает функцию из вопроса и переносим между разными семействами микроконтроллеров:

#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 Кбайт таблиц данных в дополнение к самому коду.