Как стать автором
Обновить

Комментарии 36

Вот жеж оно что. Познавательно. Спасибо. Учтем.
Тем не менее, можно с уверенностью заявить, что интерпретация содержимого регистров и памяти, как без-знаковых чисел, позволяет производить ряд операций (например, сдвиги или расширение значения) с ними проводить быстрее и порождает более компактный код, поэтому может быть настоятельно рекомендована при написании программ для МК, если иное (интерпретация как числа сщ знаком)не является обязательным условием.

Не могу сходу согласиться с таким радикальным предложением, поскольку нашли вы его эмпирически для одной конкретной ситуации.
Взгляните, например, вот на такой пример — godbolt.org/z/lW6rk8 — (courtesy в сторону презентации самого товарища Godbolt'a «What has my compiler done for me lately»).

Даже очень вдумчивое чтение стандартов С, по моему мнению, не заменяет проверки на практике. И зачастую просто смотреть на ассемблер и считать команды тоже недостаточно, особенно на архитектурах с большим конвейером, кэшом и всякими предсказателями переходов. Я понимаю, статья какбы про AVR, но в старших Cortex'ах это потихоньку появляется…
Ну это конечно, Вы совершенно правы, я только для AVR проверял и рекомендации однозначно распространяются на них, насчет остальных все непросто, как я указывал в посте про корень квадратный.
А мой пример и на AVR будет работать точно так же :) godbolt.org/z/hKWNlb
Тут как раз стандарт языка дает объяснение:
Спойлер
Переполнение знаковых целых — это неопределенное поведение! Т.е. с точки зрения компилятора это что-то такое, чего никогда не происходит. Ведь программист не допустит неопределенного поведения :)
А раз это никогда не происходит, значит обычный int не переполняется и проверку на это можно не делать.

А вот для беззнаковых переполнение точно определено, оно может произойти, поэтому компилятор вынужден вставлять проверку.

Мне кажется, что если встает вопрос того, насколько эффективно что-то будет сделано компилятором, и встает этот вопрос обоснованно — то надо пользоваться ассемблерными вставками. Может быть я не прав — допускаю.
Так и есть. Например, в коде для сигнальных процессоров довольно часто мелькают ассемблерные вставки, когда человек «показывает пальцем», как именно надо сделать.
Есть мнение, что для того, чтобы указать компилятору на работу именно с 8-бит переменными, нужно использовать конструкцию вида (byte)((byte)a + b).

В C# эта проблема даже удостоилась упоминания в «CLR via C#» Джеффри Рихтера.

Я конечно же совсем не знаю С, но пост напоминает многие статьи по JS типа "Как не сложить число со строкой".

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

А вот не соглашусь. С того же годболта: godbolt.org/z/wBK0is. Зачем в 1ой функции cmp r3,#0? Зачем во 2ой функции внутри цикла постоянно(!) выполняются условный и безусловный переходы когда это очевидным способом сокращается до одного условного перехода в цикле, а вход в цикл идёт в его середину?
Во второй функции volatile заставляет сохранять в «a» каждое значение. Поэтому всё время выполняется ldr и str. Если переписать так godbolt.org/z/bcPlpK то он уже не мучает переменную «a», но сразу сохраняет значение в «b».
Вообще компилятор под AVR почти всегда в циклах сохраняет что-то в памяти и в следующей же инструкции достаёт это, хотя регистры очевидно не менялись. И редко использует обратный цикл (в данном случае он и не мог это сделать из-за волатайл).
Во godbolt.org/z/65gS4V это его любимая фишка.
Ну так я специально написал volatile и у меня никаких претензий к тому что компилятор при этом постоянно перечитывает переменную нет.
Зачем в 1ой функции cmp r3,#0?

В эту конструкцию транслировалось выражение (while a--).
Зачем во 2ой функции[...]

Поменяйте оптимизацию на O2. Код будет больше, но будет 1 условный переход и один цикл. volatile модификатор тоже заставляет компилятор делать лишнюю, по мнению программиста, работу.
В эту конструкцию транслировалось выражение (while a--).

subs r3,r3,#1 уже выставил флаг нуля, нафиг его ещё раз генерировать при помощи cmp r3,#0? Претензия к этому.

Поменяйте оптимизацию на O2.

.L8:
ldr r3, [sp, #4]
cmp r3, r2
bgt .L6
ldr r3, [sp, #4]
adds r3, r3, #1
str r3, [sp, #4]
b .L8

Вот это сдвигом того что после bgt — в начало, а того что до — в конец, и заменой bgt на ble преобразуется в цикл без безусловного перехода. Сам безусловный переход отправляется делать вхождение в середину цикла (команды ldr/cmp, которые мы переставили в конец). Итого при ровно том же самом размере получаем выигрыш в быстродействии. Претензия именно к этому.
Конкретно здесь из за volatile, который ставят просто для того, чтобы код в режиме оптимизации просто не свернулся в 0, ведь реальной работы функция не делает.

Но, вообще то, я имел в виде не то, что компилятор любой фрагмент (в том числе простой) сделает быстрее человека, а то, что в общем человеку крайне трудно учитывать все возможные фичи системы команд (типа сдвигов операндов, формирования констант и т.д.) и, в общем, человек проигрывает.
Ну честно говоря ваш пример с известной конструкцией xor-and-xor это не показатель, просто компилятор знал про этот трюк а вы нет :)
Компилятор для AVR вообще любит чудачить с оптимизацией. Понятно, что на количество команд действует уровень оптимизации 0, 1, 2, s. Но иногда бывает так, что небольшое изменение в коде на С приводит к пропаданию десятка-другого инструкций.
Ровно год назад я мучился, пытаясь впихнуть код бутлоадера в 8К. Тогда реально наблюдал ситуации, когда указание знаков/беззнаковых типов или какая-нибудь незначительная перестановка внезапно добавляет 100 байт кода и прошивка начинает занимать 8212 байт, например. Жаль сейчас уже примеры не вспомню. Но удивляло это знатно.
С AVR не знаком, но XC8 вполне нормально оптимизирует, пишешь на Си и понимаешь что будет на Асме
НЛО прилетело и опубликовало эту надпись здесь
Я тогда много разных опций перепробовал. Самая беда была, что изначально бутлоадер со сторонней библиотекой весил 13К, потом с оптимизациями 11К. А потом пришлось взять напильник и вырезать часть библиотеки, тогда стало примерно 7600-7700.
НЛО прилетело и опубликовало эту надпись здесь
Кстати, забавно, Ваш вариант не помог — 3 такта но зато вариант
byte = uint8_t (byte << 1);
сделал минимальный код, короче, я не понимаю, как работает компилятор (ну это не новость).
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
Простите, но тема, скорее, из области «почему для AVR лучше использовать проприетарный компилятор».
IAR сразу дает однострочный результат LSL/LSR.
К сожалению, данный конкретный компилятор не поддерживает некоторые полезные фичи GCC, иначе я бы его точно рекомендовал.
А какие именно, можно узнать?
Точно не дает использовать прагму обязательности использования возврата функции, это я недавно нарвался, а с год назад еще что то было, уже и не вспомню. Но эти фичи не входят в стандарт языка (наверное), они чисто GCC.
Понятно. С другой стороны, у IAR есть свои плюшки, начисто отсутствующие в GCC. Ну и код на выходе существенно лучше.
uint16_t byteu;
byteu = byteu << 4;

У меня образовался буйный восторг от кода компилятора! В одном проекте нужно было решить именно такую задачу. Там не было ограничения ни по ресурсам, ни по времени, а хотелось сделать оптимально из эстетических соображений. На тот момент лучше 7 команд, предложенных вами, я придумать не смог, но чувствовал, что можно!
Да, настоящее программирование в чем то подобно искусству, но только настоящее.
Помимо восторга, у меня еще остается чувство «Черт, почему это придумал не я, ведь так все просто и очевидно»
Насчёт очевидности: для меня команда eor является понятной, но не интуитивно. И при первом взгляде на код, сразу начинаешь думать о каком-то подвохе: либо код вообще нерабочий, либо годен только для конкретной ситуации, ну или для некоторых входных данных, но не для всех. И только потом осознаёшь гениальность применения команды.
Однако, не только eor является для меня «неочевидной» в AVR:
до недавнего времени не использовал команды fmul, пока не потребовалось оптимизировать возведение в квадрат двухбайтного знакового числа;
cpse использую редко, хотя порой она может сэкономить пару байт;
ни разу не использовал команды ветвления на флагах N, V и H, но уверен, что есть куча случаев, где их применение оптимально;
ldd и std порой экономят очередное указание адреса;
и ещё для ijmp и icall я пока нашёл применение лишь в одном проекте.
Отмечу, что Вы (и я тоже) не являетесь «средним» пользователем МК в плане ассемблера, а достаточно много с ним работаете, и все равно всех тонкостей не используете, поэтому компилятор однозначно такого пользователя обгонит, да и нас тоже, скорее всего, особенно, если мы ему слегка поможем.
Кстати, первой моей мыслью при виде этого кода была «Да тут ошибка, это не может работать, надо срочно сообщить разработчикам», но она погасла после внимательного взгляда.
где то пробегала табличка на хабре насколько и для какой системы какой компилятор многословен… и как то мне показалось, что и не очень
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации