Оптимизация кода C (AVR). Оптимизация кода c


optimization - Оптимизация кода C (AVR)

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

Это код C:

const uint8_t amplitudes60[60] = {127, 140, 153, 166, 176, 191, 202, 212, 221, 230, 237, 243, 248, 251, 253, 254, 253, 251, 248, 243, 237, 230, 221, 212, 202, 191, 179, 166, 153, 140, 127, 114, 101, 88, 75, 63, 52, 42, 33, 24, 17, 11, 6, 3, 1, 0, 1, 3, 6, 11, 17, 24, 33, 42, 52, 63, 75, 88, 101, 114}; const uint8_t amplitudes13[13] = {127, 176, 221, 248, 202, 153, 101, 52, 17, 1, 6, 33, 75}; const uint8_t amplitudes10[10] = {127, 176, 248, 202, 101, 52, 17, 1, 33, 75}; volatile uint8_t numOfAmps = 60; volatile uint8_t *amplitudes = amplitudes60; volatile uint8_t amplitudePlace = 0; ISR(TIMER1_COMPA_vect) { PORTD = amplitudes[amplitudePlace]; amplitudePlace++; if(amplitudePlace == numOfAmps) { amplitudePlace = 0; } }

амплитуды и numOfAmps изменяются другой процедурой прерывания, которая работает намного медленнее, чем эта (в основном она запускается для изменения частот, которые воспроизводятся). В конце дня я не буду использовать эти точные массивы, но это будет очень похоже на настройку. Скорее всего, у меня будет массив с 60 значениями, а другой - всего 30. Это потому, что я строю частотную подметальную машину, а на более низких частотах я могу позволить себе больше образцов, поскольку у меня больше часов, чтобы играть, но на более высоких частотах я очень привязан к времени.

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

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

Таким образом, код при моделировании занимает 65 циклов. Опять же, мне сказали, что я смогу довести его до 30 циклов в лучшем случае.

Это код ASM, созданный с учетом того, что каждая строка делает рядом с ним:

ISR(TIMER1_COMPA_vect) { push r1 push r0 in r0, 0x3f ; save status reg push r0 eor r1, r1 ; generates a 0 in r1, used much later push r24 push r25 push r30 push r31 ; all regs saved PORTD = amplitudes[amplitudePlace]; lds r24, 0x00C8 ; r24 <- amplitudePlace I’m pretty sure lds r30, 0x00B4 ; these two lines load in the address of the lds r31, 0x00B5 ; array which would explain why it’d a 16 bit number ; if the atmega8 uses 16 bit addresses add r30, r24 ; aha, this must be getting the ADDRESS OF THE element adc r31, r1 ; at amplitudePlace in the array. ld r24, Z ; Z low is r30, makes sense. I think this is loading ; the memory located at the address in r30/r31 and ; putting it into r24 out 0x12, r24 ; fairly sure this is putting the amplitude into PORTD amplitudePlace++; lds r24, 0x011C ; r24 <- amplitudePlace subi r24, 0xFF ; subi is subtract imediate.. 0xFF = 255 so I’m ; thinking with an 8 bit value x, x+1 = x - 255; ; I might just trust that the compiler knows what it’s ; doing here rather than try to change it to an ADDI sts 0x011C, r24 ; puts the new value back to the address of the ; variable if(amplitudePlace == numOfAmps) lds r25, 0x00C8 ; r24 <- amplitudePlace lds r24, 0x00B3 ; r25 <- numOfAmps cp r24, r24 ; compares them brne .+4 ; 0xdc <__vector_6+0x54> { amplitudePlace = 0; sts 0x011C, r1 ; oh, this is why r1 was set to 0 earlier } } pop r31 ; restores the registers pop r30 pop r25 pop r24 pop r19 pop r18 pop r0 out 0x3f, r0 ; 63 pop r0 pop r1 reti

Кроме того, возможно, используя меньше регистров в прерывании, чтобы у меня было меньше push/pops, я действительно не вижу, где этот код сборки неэффективен.

Моя единственная другая мысль - возможно, оператор if может быть освобожден, если бы я мог решить, как получить n-битовый int-тип данных в C, чтобы число обернулось, когда оно достигнет конца? Под этим я имею в виду, что у меня было бы 2 ^ n - 1 выборки, а затем переменная амплитуды Place просто продолжала бы подсчитывать, так что, когда она достигнет 2 ^ n, она переполнится и будет reset равна нулю.

Я действительно пытался имитировать код без бит if, и, хотя он улучшил скорость, потребовалось всего около 10 циклов, так что это было примерно 55 циклов для одного исполнения, которое, к сожалению, еще не достаточно быстро, Мне нужно оптимизировать код еще больше, что сложно рассмотреть, так как это всего лишь 2 строки!

Моя единственная реальная мысль заключается в том, чтобы увидеть, могу ли я хранить таблицы статического поиска где-нибудь, для чего требуется меньше тактовых циклов? Инструкции LDS, которые он использует для доступа к массиву, я думаю, что все принимают 2 цикла, поэтому я, вероятно, не собираюсь много экономить много времени, но на этом этапе я готов попробовать что-нибудь.

Я совершенно не понимаю, куда идти отсюда. Я не вижу, как я мог бы сделать свой C-код более эффективным, но я только новичок в этом, поэтому я мог что-то упустить. Я бы хотел, чтобы какая-то помощь.. Я понимаю, что это довольно специфическая и связанная с этим проблема, и обычно я стараюсь избегать задавать такие вопросы здесь, но я долгое время работал над этим, Я действительно сделаю любую помощь, которую я смогу получить.

qaru.site

Оптимизация кода

 

Самая актуальная документация по Visual Studio 2017: Документация по Visual Studio 2017.

С помощью оптимизации исполняемого файла можно добиться баланса между высокой скоростью выполнения и небольшим размером кода. В данном разделе рассматриваются некоторые из доступных в Visual C++ механизмов, облегчающих оптимизацию кода.

В следующих разделах описываются некоторые из возможностей оптимизации, доступных в языке C/C++.

Директивы pragma и ключевые слова оптимизацииСписок ключевых слов и прагм, которые можно использовать в коде для повышения производительности.

Параметры компилятора, упорядоченные по категориямСписок параметров компилятора /O, которые непосредственно влияют на скорость выполнения или размер кода.

Декларатор ссылки Rvalue: &&Ссылки rvalue поддерживают реализацию семантики перемещения. Применение семантик перемещения для реализации библиотек шаблонов может значительно повысить производительность приложений, в которых эти шаблоны используются.

Pragma оптимизации

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

Следует поместить код между двумя прагмами, как показано далее.

#pragma optimize("", off) #pragma optimize("", on)

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

Парадоксально, но оптимизация быстродействия программы может в отдельных случаях вызвать замедление быстродействия кода. Это происходит из-за того, что некоторые процессы оптимизации быстродействия увеличивают объем кода. Например, встраивание функций может свести к минимуму дополнительные издержки при вызове функций. Однако вследствие встраивания слишком большого объема кода размер программы может увеличиться настолько, что это приведет к увеличению числа ошибок страницы виртуальной памяти. Таким образом, увеличение быстродействия вследствие отказа от вызовов функции может привести к потерям в памяти подкачки.

В следующих разделах рассматриваются рекомендуемые методы программирования.

Рекомендации по оптимизации срочного кодаБолее совершенные методы написания кода могут помочь повысить производительность. В этом разделе приведены методы написания кода, которые помогают убедиться, что срочные части кода выполняются удовлетворительно.

Рекомендации по оптимизацииОбщие правила оптимизации приложения.

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

В следующих разделах содержатся основные сведения об отладке.

В следующих разделах содержатся более подробные сведения об отладке.

В следующем наборе разделов приведена информация об оптимизации процессов построения, загрузки и выполнения кода.

Образец построения C/C++

technet.microsoft.com

c++ - Варианты оптимизации Visual С++ - как улучшить вывод кода?

К сожалению, я не смог найти способ значительно улучшить вывод Visual С++ в этом случае, даже используя более агрессивные флаги оптимизации. Кажется, что есть несколько факторов, способствующих неэффективности VS, включая отсутствие определенных оптимизаций компилятора и структуру реализации Microsoft <vector>.

Проверяя сгенерированную сборку, Кланг делает объективно замечательную работу по оптимизации этого кода. В частности, по сравнению с VS, Clang может выполнять очень эффективное распространение Constant, Function Inlining (и, следовательно, Dead Code Elimination) и оптимизацию New/delete.

Постоянное распространение

В примере вектор статически инициализируется:

std::vector<int> v = {1, 2, 3, 4};

Обычно компилятор будет хранить константы 1, 2, 3, 4 в памяти данных, а в цикле for будет загружать одно значение по одному за раз, начиная с низкого адреса, в котором хранится 1, и добавьте каждое значение в сумму.

Вот сокращенный код VS для этого:

movdqa xmm0, XMMWORD PTR __xmm@00000004000000030000000200000001 ... movdqu XMMWORD PTR $T1[rsp], xmm0 ; Store integers 1, 2, 3, 4 in memory ... $LL4@main: add ebx, DWORD PTR [rdx] ; loop and sum the values lea rdx, QWORD PTR [rdx+4] inc r8d movsxd rax, r8d cmp rax, r9 jb SHORT $LL4@main

Однако Кланг очень умный, чтобы понять, что сумма может быть рассчитана заранее. Мое лучшее предположение заключается в том, что он заменяет загрузку констант из памяти в константные операции mov в регистры (распространяет константы), а затем объединяет их в результат 10. Это имеет полезный побочный эффект от разрыва зависимостей, а так как адреса больше не загружаются, компилятор может удалить все остальное как мертвый код.

Clang, по-видимому, уникален в этом - ни VS, ни GCC не смогли заранее предсказать результат накопления вектора.

Новая/Удалить Оптимизация

Составители, соответствующие С++ 14, могут опускать вызовы на новые и удалять при определенных условиях, в частности, когда количество вызовов распределения не является частью наблюдаемого поведения программы (N3664 стандартная бумага). Это уже вызвало много дискуссий по SO:

Clang, вызываемый с помощью -std=c++14 -stdlib=libc++, действительно выполняет эту оптимизацию и исключает вызовы для новых и удаления, которые несут побочные эффекты, но, предположительно, не влияют на наблюдаемое поведение программы. С помощью -stdlib=libstdc++ Clang более строг и сохраняет вызовы на новые и удаляет - хотя, глядя на сборку, ясно, что они действительно не нужны.

Теперь, проверяя код main, сгенерированный VS, мы можем найти там два вызова функций (с остальной конструкцией вектора и итерационный код, заключенный в main):

call std::vector<int,std::allocator<int> >::_Range_construct_or_tidy<int const * __ptr64>

и

call void __cdecl operator delete(void * __ptr64)

Первый используется для выделения вектора, а второй для его освобождения, и практически все другие функции на выходе VS вытягиваются этими вызовами функций. Это означает, что Visual С++ не будет оптимизировать вызовы функций распределения (для соответствия С++ 14 мы должны добавить флаг /std:c++14, но результаты будут одинаковыми).

Этот сообщение в блоге (10 мая 2017 года) из команды Visual С++ подтверждает, что эта оптимизация не реализована. Поиск на странице N3664 показывает, что "Предотвращение/сплайсинг распределения" находится в статусе N/A, а связанный комментарий говорит:

[E] Разрешения для предотвращения/слияния разрешены, но не требуются. В настоящее время мы решили не реализовывать это.

Объединяя оптимизацию нового/удаления и постоянное распространение, легко увидеть влияние этих двух оптимизаций в этом Компилятор Explorer Трехстороннее сравнение клана с -stdlib=libc++, Clang с -stdlib=libstdc++ и GCC.

Выполнение STL

VS имеет свою собственную реализацию STL, которая сильно отличается от libС++ и stdlibС++, и это, похоже, вносит большой вклад в поколение кода VS. Хотя VS STL имеет некоторые очень полезные функции, такие как проверенные итераторы и отладчики отладки итератора (_ITERATOR_DEBUG_LEVEL), мое общее впечатление о том, что он более структурирован и менее эффективен, чем stdlibС++.

Для выделения влияния реализации векторной STL интересным экспериментом будет использование Clang для компиляции в сочетании с файлами заголовков VS. Действительно, использование Clang 5.0.0 с заголовками Visual Studio 2015 приводит к следующему генерации кода - очевидно, реализация STL имеет огромное влияние!

main: # @main .Lfunc_begin0: .Lcfi0: .seh_proc main .seh_handler __CxxFrameHandler3, @unwind, @except # BB#0: # %.lr.ph pushq %rbp .Lcfi1: .seh_pushreg 5 pushq %rsi .Lcfi2: .seh_pushreg 6 pushq %rdi .Lcfi3: .seh_pushreg 7 pushq %rbx .Lcfi4: .seh_pushreg 3 subq $72, %rsp .Lcfi5: .seh_stackalloc 72 leaq 64(%rsp), %rbp .Lcfi6: .seh_setframe 5, 64 .Lcfi7: .seh_endprologue movq $-2, (%rbp) movl $16, %ecx callq "??2@YAPEAX_K@Z" movq %rax, -24(%rbp) leaq 16(%rax), %rcx movq %rcx, -8(%rbp) movups .L.ref.tmp(%rip), %xmm0 movups %xmm0, (%rax) movq %rcx, -16(%rbp) movl 4(%rax), %ebx movl 8(%rax), %esi movl 12(%rax), %edi .Ltmp0: leaq -24(%rbp), %rcx callq "?_Tidy@?$vector@HV?$allocator@H@std@@@std@@IEAAXXZ" .Ltmp1: # BB#1: # %"\01??1?$vector@HV?$allocator@H@std@@@std@@[email protected]" addl %ebx, %esi leal 1(%rdi,%rsi), %eax addq $72, %rsp popq %rbx popq %rdi popq %rsi popq %rbp retq .seh_handlerdata .long ($cppxdata$main)@IMGREL .text

Обновление - Visual Studio 2017

В Visual Studio 2017, <vector> просмотрел крупный капитальный ремонт, как было объявлено в этом сообщении в блоге от команды Visual С++. В частности, он упоминает следующие оптимизации:

Любопытный, я вернулся, чтобы проверить это. При построении примера в Visual Studio 2017 результат остается списком сборок на нескольких страницах с множеством вызовов функций, поэтому даже при улучшении генерации кода это трудно заметить.

Однако при создании с заголовками clang 5.0.0 и Visual Studio 2017 мы получаем следующую сборку:

main: # @main .Lcfi0: .seh_proc main # BB#0: subq $40, %rsp .Lcfi1: .seh_stackalloc 40 .Lcfi2: .seh_endprologue movl $16, %ecx callq "??2@YAPEAX_K@Z" ; void * __ptr64 __cdecl operator new(unsigned __int64) movq %rax, %rcx callq "??3@YAXPEAX@Z" ; void __cdecl operator delete(void * __ptr64) movl $10, %eax addq $40, %rsp retq .seh_handlerdata .text

Обратите внимание на инструкцию movl $10, %eax, то есть с 2017 <vector>, clang удалось свернуть все, предварительно вычислить результат 10 и сохранить только вызовы для новых и удалить.

Я бы сказал, что это потрясающе!

Функция Inlining

Функциональная вставка, вероятно, является самой важной в этом примере оптимизацией. При свертывании кода вызываемых функций на свои сайты вызова компилятор может выполнять дальнейшие оптимизации по объединенному коду, а также удаление вызовов функций выгодно в сокращении накладных расходов и устранении барьеров оптимизации.

При проверке сгенерированной сборки для VS и сравнении кода до и после встраивания (Compiler Explorer), мы видим, что большинство векторных функции были действительно включены, за исключением функций распределения и освобождения. В частности, есть призывы к memmove, которые являются результатом вложения некоторых функций более высокого уровня, таких как _Uninitialized_copy_al_unchecked.

memmove является библиотечной функцией и поэтому не может быть встроенной. Тем не менее, clang имеет умный способ обойти это - он заменяет вызов на memmove вызовом __builtin_memmove. __builtin_memmove - встроенная/внутренняя функция, имеющая ту же функциональность, что и memmove, но в отличие от вызова простой функции компилятор генерирует для нее код и вставляет его в вызывающую функцию. Следовательно, код может быть дополнительно оптимизирован внутри вызывающей функции и в конечном итоге удален как мертвый код.

Резюме

В заключение, Clang явно превосходит VS в этом примере, как благодаря оптимизации высокого качества, так и более эффективной реализации векторной STL. При использовании одних и тех же заголовочных файлов для Visual С++ и clang (заголовки Visual Studio 2017) Clang превосходит Visual С++.

Во время написания этого ответа я не мог не думать, что бы мы сделали без Компилятоp > ? Спасибо Matt Godbolt за этот замечательный инструмент!

qaru.site


Prostoy-Site | Все права защищены © 2018 | Карта сайта