Comments 45
Я вот подумал опечатка… А потом думаю в контексте «изначальный дизайн настолько унылый» так и было задумано?
. И приоритет масштабируемости и производительности не стоит на первом месте.
Любое безапелляционное утверждение — неверно. Включая мое. :)
Иногда, все таки стоит на первом месте. У Кнута, как Вы сами обратили внимание, речь шла о микрооптимизациях
Если бы всё было так просто. С одной стороны, Кнут прав. С другой стороны — быстрая программа состоит из быстрых кусочков. Если программу писали опытные разработчики, но не думали о производительности постоянно, то получится алгоритмически оптимальная, но равномерно медленная программа — без каких-то видимых медленных участков кода.
Я не про микрооптимизации, если что. 95% быстродействия приложения — это алгоритмы и архитектурные решения (ЯП, используемые библиотеки, сетевые протоколы, подход к проектированию БД...).
Наблюдение: переход с питона на Rust вызвал "зуд оптимизации". На питоне всё было понятно — пока оно "достаточно быстро" работает, дальше нет смысла бороться за ускорение. Всё равно супербыстро не получится.
А вот на Rust всё не так, и есть постоянный зуд делать "супербыстро". Какие dyn? Вы что, это же дополнительный lookup? Не-не-не, Box это медленно, давайте лучше на стеке. Какой Arc, вы что? Давайте лучше исхитримся без Arc!
Это как педаль газа в спорткаре. Пока едешь на машине с двигателем 0.25 литра, газуешь ровно настолько, насколько нужно, чтобы ехать. А на спорткаре хочется почувствовать скорость (ускорение!).
А вот вам запрещённая цитата из этой самой статьи, про этот самый кусок кода. Эту цитату, почему-то, обычно не цитируют.
The improvement in speed from Example 2 to Example 2a is only about 12%, and many people would pronounce that insignificant.
The conventional wisdom shared by many of today's software engineers calls for ignoring efficiency in the small; but I believe this is simply an overreaction to the abuses they see being practiced by pennywise-and-pound-foolish programmers, who can't debug or maintain their "optimized" programs.
In established engineering disiplines a 12 % improvement, easily obtained, is never considered marginal; and I believe the same viewpoint should prevail in software engineering~ Of course I wouldn't bother making such optimizations on a oneshot job, but when it's a question of preparing quality programs, I don't want to restrict myself to tools that deny me such efficiencies
И примерный перевод:
Увеличение скорости в этом примере составляет где-то 12% и многие сочтут его незначительным. Общепринятое мнение, которго придерживаются многие разработчики призывает игнорировать эффективность в мелочах; но я считаю, что это просто чрезмерная реакция на злоупотребления которые допускают всякие разработчики, которые потом не способны поддерживать или отлаживать их "оптимизированные" программы.
В сложившейся инженерной дисциплине 12 процентное улучшение, легко получаемое, никогда не сочтут незначительным; и я думаю, что та же точка зрения должна преобладать в разработке программного обеспечения.
Конечно, я не стал бы делать такие оптимизации в одноразовой задаче, но когда встаёт вопрос о подготовке качественных программ, я не хочу ограничивать себя инструментарием, который не даёт мне таких возможностей.
Живите с этим )))
эффективный код без использования GOTO. Сейчас это кажется самоочевидным.
Сейчас просто используют Exception и try catch, что по сути является GOTO в современном мире, так что это не есть самоочевидным :)
Это не является GOTO. С тем же успехом можно записать break и return как GOTO. Главное отличие — GOTO требует указание точного места, куда передать управление. Ни исключение, ни конструкции типа return этого не требуют.
static inline uint8_t _getCycleCount8d8(void) {
uint32_t ccount;
__asm__ __volatile__("rsr %0,ccount":"=a" (ccount));
return ccount>>3;
}
#define READ_BOTH_PINS ((GPIO.in&RD_MASK)>>RD_SHIFT)
//...
// dangerous option, but faster and works with low cpu freq ~80MHz . If we have noise on bus it can overflow received_NRZI_buffer[]
void sendRecieveNParse()
{
register uint32_t R3;
register uint16_t *STORE = received_NRZI_buffer;
__disable_irq();
sendOnly();
register uint32_t R4;
START:
R4 = READ_BOTH_PINS;
*STORE = R4 | _getCycleCount8d8();
STORE++;
R3 = R4;
if( R3 )
{
for(int k=0;k<TOUT;k++)
{
R4 = READ_BOTH_PINS;
if(R4!=R3) goto START;
}
}
__enable_irq();
received_NRZI_buffer_bytesCnt = STORE-received_NRZI_buffer;
}
Фактически — это ассемблер. Код читает два GPIO pins для декодирования пакета USB. Тут нужно не быстрее, а в строго определенной последовательности с определенным таймингом.
Собирается и для АРМ M0-4(c другим _getCycleCount8d8) и для XTENSA(espressif )
Максимум что можно компилятору — поменять местами «STORE++;» и «R3 = R4;» для разбиения зависимости регистров. Но с этим справляется и сам процессор.
'read_loop: loop {
r4 = read_both_pins();
*store = r4 | get_cycle_count_8d8();
*store += 1;
r3 = r4;
if r3 != 0 {
for _ in 0..TOUT {
r4 = read_both_pins();
if r4 != r3 {
continue 'read_loop;
}
}
}
break;
}
Ну и зачем тут goto
?
А чем отличается "continue 'read_loop;" от "goto START"?
Очевидно, тем что goto может перейти куда угодно, а continue — только на следующую итерацию цикла.
А почему тогда for скипнулся? и зачем тогда указывать "'read_loop;"? разве "read_loop" не является в данном случае меткой? по мне так код сохранил переход на указанную метку, те алгоритмически ничем не отличается от первоисходника с goto.
А почему тогда for скипнулся?
Потому что указали 'read_loop
и зачем тогда указывать "'read_loop;"?
Чтобы перейти к началу цикла loop, а не for
по мне так код сохранил переход на указанную метку, те алгоритмически ничем не отличается от первоисходника с goto
Алгоритмически — и правда не отличается. Но вот читать его может оказаться проще, потому что увидев оператор continue я знаю где надо искать метку к нему, и где её искать бесполезно.
Это были риторические вопросы :) Видимо плохо выразил свою мысль. goto это передача управления на указанную метку. в примере 'read_loop это метка, continue 'read_loop это переход на указанную метку. Те завуалировав goto мы не меняем сути, те производим переход на указанную метку. те тот же goto только в другом синтаксическом сахаре.
По поводу читать легче это очень субьективно, и не одно поколение ломает по этому поводу копья ;-)
Я не занимаюсь настолько низкоуровневыми вещами, так что заранее прошу прощения если упускаю какие-то детали (меня смутил момент про тайминги в однопоточном коде) или мои эстетические чувства не совпадают с вашими. Возможно, это связано с тем, что я не видел goto в коде уже лет 15. Тем не мнее, мне было совершенно не понятно что делает ваш код до того, как я взялся его рефакторить.
Сейчас же абсолютно очевидно, что мы просто читаем значения из одного источника и перекладываем их в другой пока не наткнемся на последовательность из TOUT одинаковых ненулевых значений или ноль. При нахождении нуля заменяем его на результат функции _getCycleCount8d8 и завершаем цикл.
Также, теперь стала заметна одна странная вещь. Она присутствует и в вашем и в моём варианте: если мы встречаем последовательность из меньше чем TOUT значений, то первое отличающееся значение не попадает в STORE. Опять же, я могу что-то упускать, но мне это не кажется корректным поведением программы. Такие неочевидные моменты стоит всегда снабжать комментариями, чтобы снизить когнитивную нагрузку на читателя (даже если goto ему привычен и более понятен).
void sendRecieveNParse()
{
register uint32_t R3;
register uint16_t *STORE = received_NRZI_buffer;
__disable_irq();
sendOnly();
register uint32_t R4;
while (true)
{
R4 = READ_BOTH_PINS;
if (R4 == 0)
{
*STORE = _getCycleCount8d8();
STORE++;
break;
}
*STORE = R4;
STORE++;
R3 = R4;
int count = 0;
while (R4 == R3 && count < TOUT)
{
R4 = READ_BOTH_PINS;
count++;
}
if (count == TOUT)
{
break;
}
}
__enable_irq();
received_NRZI_buffer_bytesCnt = STORE-received_NRZI_buffer;
}
первое отличающееся значение не попадает в STORE
Насколько я вижу, код написан в предположении, что значение на входе меняется куда медленнее чем код исполняется. То есть после смены значения следующее чтение точно даст точно такое же, и ничего не пропадёт.
Если видим ноль, то заменяем его на результат функции _getCycleCount8d8.
А вот тут вы глупость написали. Обратите внимание, что в коде стоит побитовое "ИЛИ", а не логическое.
меняется куда медленнее
Цикл должен быть быстрее как минимум вдвое с мелочью. (Найквист/ Котельников)
То есть после смены значения следующее чтение точно даст точно такое же
Необязательно. Это немного сложнее. Ниже написал — почему.
А вот тут вы глупость написали. Обратите внимание, что в коде стоит побитовое «ИЛИ», а не логическое.
Извините, но «0 | N» всегда N
Извините, но "1 | N" не всегда "1"
Если видим ноль, то заменяем его на результат функции _getCycleCount8d8.
Если вам есть что сказать по теме обсуждения — welcome, иначе — на том и закончим, я не испытываю удовльствия от беребранок с незнакомцами.
Ау, вы в своём отредактированном варианте для ненулевого R4 делаете *STORE = R4;
, хотя исходный алгоритм делал *STORE = R4 | _getCycleCount8d8();
. Это неэквивалентные действия.
Как вы могли заметить, я признал свою ошибку в другой ветке. sdima1357 указал на неё четко и ясно. Вы же решили меня оскорбить сославшись на абсолютно корректное утверждение. Также прошу заметить, что указанный участок кода не имеет никакого отношения к обсуждаемой теме, так зачем сейчас весь этот разговор?
Отлично, я рад, что мы вернулись к теме обсуждения. Давайте теперь предметно.
Изначальное утверждение, с которого началась эта ветка: читать с break проще, потому что сразу понятно, что эта инструкция находится в цикле и для понимания контекста достаточно посмотреть в начало текущего блока.
Почему, по вашему мнению, с goto проще?
Здесь же мы обсуждаем использование goto в коде.
Вы описали конкретный алгоритм, давайте по нему пройдемся, и вы скажете что именно в приведенном варианте не работает «от слова совсем»?
1.
Если оба бита шины в 0 — то выйти
if (R4 == 0) break;
2.
Дальше ждем TOUT изменения состояния шины.
while (true)
{
... // "читаем еще раз ее состояние и пишем в STORE" находится здесь
count = 0;
while (R4 == R3 && count < TOUT)
{
R4 = READ_BOTH_PINS;
count++;
}
...
}
3.
Если не изменилась — то выходим.
if (count == TOUT) break;
Я понял. Конструктива не будет.
В таком случае, позволю себе ответить за вас. 40+ лет назад считалось нормально писать с goto, вы этому научены с детства, привыкли так писать и не собираетесь менять свои привычки по причине того, что молодое поколение этого не застало.
Также, напоследок, хочу напомнить, что goto — это только частный пример того, о чём говорится в обсуждаемой статье. У каждого кода есть причина по которой он написан именно таким образом, каким написан. Тот факт, что ваш код непонятен другим людям говорит о том, что при его написании для вас более приоритетно было что-то другое. Если это достаточно важно — ок, если нет — вы просто один из тех инженеров, о которых говорит автор. Это можете знать только вы.
На сим, заканчиваю этот разговор. Желаю вам успехов с вашей первой в мире реализацией и благодарю за минусы в карму. Доброй ночи
В идеале — биты меняются синхронно (один вход — это инверсия второго) кроме конца пакета, когда оба в 0. На самом деле может быть небольшой сдвиг по фазе (не совсем идентичные цепи DP/DM) и шина меняется не совсем синхронно. Для этого и нужно повторное чтение. Посмотрите картинку en.wikipedia.org/wiki/USB_(Communications)
То есть код записывает изменения на шине и выходит по таймоуту при отсутствии изменений или по нулям на обоих проводах
Код вот из этого проекта github.com/sdima1357/esp32_usb_soft_host
Тем не менее, я не ставил перед собой задачи разобраться с тем как работает USB. Мы обсуждали написание кода с goto и без. И если оставить запись в STORE как в оригинальном варианте, тогда останется сравнить два альтернативных варианта ожидания смены значения (хоть я и не уверен, что это хорошая идея мерять время итерациями цикла):
for(int k=0;k<TOUT;k++)
{
R4 = READ_BOTH_PINS;
if(R4!=R3) goto START;
}
int count = 0;
while (R4 == R3 && count < TOUT)
{
R4 = READ_BOTH_PINS;
count++;
}
if (k == TOUT)
{
break;
}
И, как по мне, второй вариант проще как раз по той причине, которую озвучил gridem: break позволяет без траты времени на поиск метки понять что происходит.
Чисто позанудствовать
Тем не менее, рассмотренные примеры говорили, что GOTO дает выигрыш. Программа без GOTO становится более простой, однако цена — это скорость исполнения.
Рассмотрим примеры из статьи
Оба примера кода (2a и 2b) иллюстрируют алгоритм БЕЗ GOTO :)
2b — не про GOTO, а про ручную оптимизацию цикла. Увеличиваем индекс через 2, и читаем по 2 элемента на каждой итерации. То, что в 2b фигурирует GOTO — это автор так пытался сделать свою мысль более понятной для определенной аудитории. В оригинальной статье это звучит как:
And if Example 2 were really critical, I would improve on it still more by "doubling it up" so that the machine code would be essentially as follows
что можно примерно перевести как
Я бы мог еще больше улучшить программу путем "удвоения" цикла, так что ее ее машинный код [после компиляции — прим. переводчика] будет выглядеть как-то так
Предварительная оптимизация — корень всех зол?