Можно ли оптимизировать этот вложенный l oop? - PullRequest
0 голосов
/ 05 февраля 2020

Я работаю над проектом, в котором я хочу преобразовать данный видеовход в разделы блока (чтобы он мог использоваться аппаратным кодом c). Этот проект выполняется на микроконтроллере STM32 с тактовой частотой 200 МГц.

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

Size:      32 bit word    32 bit word    32 bit word    ...
Component: Cr Y1 Cb Y0    Cr Y1 Cb Y0    Cr Y1 Cb Y0    ...
Bits:      8  8  8  8     8  8  8  8     8  8  8  8     ...

Этот поток необходимо преобразовать в формат блока, используемый аппаратным кодом c. Код c принимает байтовый массив в указанном c порядке. В настоящее время я делаю это, используя вложенную l oop для каждой 1/8 кадра изображения, используя таблицы поиска и записывая в пустой массив:

Определяет:

#define ROWS_PER_MCU                    8
#define WORDS_PER_MCU                   8
#define HORIZONTAL_MCU_PER_INPUTBUFFER  40
#define VERTICAL_MCU_PER_INPUTBUFFER    8

Глобальные переменные объявлены так:

typedef struct jpegInputbufferLUT
{
    uint8_t JPEG_Y_MCU_LUT[256];
    uint8_t JPEG_Cb_MCU_422_LUT[256];
    uint8_t JPEG_Cr_MCU_422_LUT[256];
}jpegIndexLUT;

jpegIndexLUT jpegInputLUT;

uint8_t jpegInBuffer[81920];
uint32_t rawBuffer[20480];

Посмотрите таблицы создаются следующим образом:

void JPEG_Init_MCU_LUT(void)
{
    uint32_t offset;

    /*Y LUT */
    for(uint32_t i = 0; i < 16; i++)
    {
        for(j = 0; j < 16; j++)
        {
            offset =  j + (i*8);
            if((j>=8) && (i>=8)) offset+= 120;
            else  if((j>=8) && (i<8)) offset+= 56;
            else  if((j<8) && (i>=8)) offset+= 64;

            jpegInputLUT.JPEG_Y_MCU_LUT[i*16 + j] = offset;
        }
    }

    /*Cb Cr LUT*/
    for(uint32_t i = 0; i < 16; i++)
    {
        for(j = 0; j < 16; j++)
        {
            offset = i*16 + j;

            jpegInputLUT.JPEG_Cb_MCU_422_LUT[offset] = (j/2) + (i*8) + 128;

            jpegInputLUT.JPEG_Cr_MCU_422_LUT[offset] = (j/2) + (i*8) + 192;
        }
    }
}

Код преобразования:

/* Initialize variables for array conversion */
uint32_t currentMCU = 0;
uint32_t lutOffset = 0;
uint32_t inputOffset = 0;
uint32_t verticalOffset = 0;

/* Convert X rows into MCU blocks for JPEG encoding */
for(uint8_t k = 0; k < VERTICAL_MCU_PER_INPUTBUFFER; k++)
{
    for(uint8_t n = 0; n < HORIZONTAL_MCU_PER_INPUTBUFFER; n++)
    {
        inputOffset = verticalOffset + (n * 8);
        lutOffset = 0;

        for(uint8_t i = 0; i < ROWS_PER_MCU; i++)
        {
            for(uint8_t j = 0; j < WORDS_PER_MCU; j++)
            {
                /* Mask 32 bit according to DCMI input format */
                uint32_t rawBufferAddress = inputOffset+j; // Calculate rawBuffer address here so it only has to be calculated once
                jpegInBuffer[jpegInputLUT.JPEG_Y_MCU_LUT[lutOffset] + currentMCU]       = (rawBuffer[rawBufferAddress] & 0x7F);
                jpegInBuffer[jpegInputLUT.JPEG_Cb_MCU_422_LUT[lutOffset] + currentMCU]  = ((rawBuffer[rawBufferAddress] >> 7) & 0x7F);
                jpegInBuffer[jpegInputLUT.JPEG_Cr_MCU_422_LUT[lutOffset] + currentMCU]  = ((rawBuffer[rawBufferAddress] >> 23) & 0x7F);
                jpegInBuffer[jpegInputLUT.JPEG_Y_MCU_LUT[lutOffset+1] + currentMCU]     = ((rawBuffer[rawBufferAddress] >> 16) & 0x7F);

                lutOffset+=2;
            }
            inputOffset += 320;
        }
        currentMCU += 256;
    }
    verticalOffset += 2240;
}

Это преобразование в настоящее время принимает меня о 8 мс, и это нужно сделать 8 раз. В настоящее время это занимает почти все мое доступное время выполнения, так как я пытаюсь получить 15 кадров в секунду из моей системы.

Возможно ли каким-либо образом ускорить это? Я думал, может быть, сортировка входного массива вместо простой записи в новый буфер, но будет ли обмен 2 элементов в массиве быстрее, чем копирование значений в другой массив?

Хотелось бы услышать ваши идеи / мысли по этому поводу,

Заранее спасибо!

Ответы [ 2 ]

2 голосов
/ 06 февраля 2020
  1. Ваша программа работает медленнее, чем ожидалось от STM32. Возможно, вам придется посмотреть, какая сборка производится, настройки оптимизации компилятора, если частота MCU верна, если память слишком медленная, и т. Д. c. У нас недостаточно информации, чтобы дать определенный ответ, почему. Кажется, ваш код тратит 8 мс * 200 м / (8 * 8 * 8 * 40) = 78 циклов на каждую внутреннюю итерацию l oop. Для справки, stm32f723 требуется всего около 15 циклов, а stm32f103 - около 28 циклов (в последнем случае код был настроен для доступа к меньшим массивам).

  2. Таблица LUT не нужна так как его содержание очень регулярное. Чтение значений LUT добавляет больше операций чтения из памяти, что может быть значительным вкладом. Если я правильно получил ваш код генерации LUT, он выдает следующие числа во внутреннем l oop:

Y1  Cb  Cr  Y2
0   128 192 1
2   129 193 3
4   130 194 5
6   131 195 7
64  132 196 65
66  133 197 67
68  134 198 69
70  135 199 71
8   136 200 9
etc

Второй и третий столбцы просто последовательные номера. Четвертый столбец равен первому плюс один. И первое число нужно немного перевернуть. Вы можете попробовать следующий код (пожалуйста, убедитесь, что он правильный):

uint32_t lutOffset = 0;
for(uint8_t i = 0; i < ROWS_PER_MCU; i++)
{
    for(uint8_t j = 0; j < WORDS_PER_MCU; j++)
    {
        uint32_t rawBufferAddress = (inputOffset+j) /* % 2048 */;
#if 0
        unsigned y_lut1 = jpegInputLUT.JPEG_Y_MCU_LUT[lutOffset];
        unsigned Cb_lut = jpegInputLUT.JPEG_Cb_MCU_422_LUT[lutOffset];
        unsigned Cr_lut = jpegInputLUT.JPEG_Cr_MCU_422_LUT[lutOffset];
        unsigned y_lut2 = jpegInputLUT.JPEG_Y_MCU_LUT[lutOffset+1];
#else
        unsigned y_lut1 = lutOffset | (j / 4) << 6 | (j % 4) << 1;
        unsigned Cb_lut = 128 + lutOffset + j;
        unsigned Cr_lut = 192 + lutOffset + j;
        unsigned y_lut2 = y_lut1 + 1;
#endif
        jpegInBuffer[y_lut1 + currentMCU] = (rawBuffer[rawBufferAddress] & 0x7F);
        jpegInBuffer[Cb_lut + currentMCU] = ((rawBuffer[rawBufferAddress] >> 7) & 0x7F);
        jpegInBuffer[Cr_lut + currentMCU] = ((rawBuffer[rawBufferAddress] >> 23) & 0x7F);
        jpegInBuffer[y_lut2 + currentMCU] = ((rawBuffer[rawBufferAddress] >> 16) & 0x7F);
    }
    lutOffset += 8;
    inputOffset += 320;
}

Эта версия занимает около 20 циклов на итерацию на моем stm32f103, что составляет менее 6 мс даже при 72 МГц.

UPD. Другой вариант - использовать одну небольшую справочную таблицу вместо битовых вычислений:

static const unsigned x[8] = { 0, 2, 4, 6, 64, 66, 68, 70 };

//  unsigned y_lut1 = lutOffset | (j / 4) << 6 | (j % 4) << 1;
  unsigned y_lut1 = lutOffset + x[j];

Это улучшает внутреннюю синхронизацию l oop до 18 (f103) / 7.5 (f723) циклов. По какой-то причине оптимизация этого выражения для F723 не работает должным образом. Я ожидал бы, что эти опции дадут идентичный результат, так как внутренний l oop развернут, но кто знает.

В качестве дополнительной оптимизации, которая, вероятно, не требуется, выходные значения могут быть объединены в 32-разрядные слова и записаны по одному слову за раз. Это кажется возможным, потому что значения LUT входят в блоки из четырех последовательных. Для этого внутренний l oop может быть преобразован во вложенный l oop из 2 на 4 итерации. Каждые 4 итерации самой внутренней l oop будут давать один uint32_t для Cb, один uint32_t для Cr и два uint32_t для Y. Но делать это не стоит.

Я измеряю время выполнения с SysTick:

SysTick->LOAD = SysTick_LOAD_RELOAD_Msk;
SysTick->VAL = 0;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_ENABLE_Msk;

volatile unsigned t0 = SysTick->VAL;
f();
volatile unsigned t1 = t0 - SysTick->VAL;

Я тоже иногда использовал выходные выводы, когда подключение отладчика нецелесообразно. Строго говоря, оба метода не гарантированно работают, потому что компилятор может перемещать код между точками измерения, но он работал так, как мне предназначалось (с g cc). Проверка сборки необходима, чтобы убедиться, что ничего подозрительного не происходит.

1 голос
/ 05 февраля 2020

Здесь можно выполнить любое количество микрооптимизаций, которые могли бы обеспечить улучшение. Некоторые могут демонстрировать улучшение в отладочной сборке без оптимизации компилятора, но не имеют преимуществ при оптимизации. Возможно даже, что какой-то «умный» трюк, который быстрее в отладке, если не-идиоматизм c может заставить оптимизатор генерировать худший код, который мог бы дать вам ясность по сравнению с производительностью.

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

Одно довольно очевидное улучшение (независимо от того, быстрее оно или нет), было бы изменить:

        for( uint8_t j = 0; j < WORDS_PER_MCU; j++ )
        {
            /* Mask 32 bit according to DCMI input format */
            uint32_t rawBufferAddress = inputOffset+j; // Calculate rawBuffer address here so it only has to be calculated once
            ...

на:

        uint32_t rawBufferAddress = inputOffset ;
        for( uint8_t j = 0; j < WORDS_PER_MCU; rawBufferAddress++, j++)
        {
            /* Mask 32 bit according to DCMI input format */
            ...

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

Я бы также предложил перенести все остальные "приращения конца l oop, такие как lutOffset+=2, в соответствующее третье выражение for. Не для производительности, но для ясности.

...