Pull to refresh

Comments 45

> масштабировать изначальную рахитектуру
Я вот подумал опечатка… А потом думаю в контексте «изначальный дизайн настолько унылый» так и было задумано?

Это была шутка юмора.

. И приоритет масштабируемости и производительности не стоит на первом месте.

Любое безапелляционное утверждение — неверно. Включая мое. :)
Иногда, все таки стоит на первом месте. У Кнута, как Вы сами обратили внимание, речь шла о микрооптимизациях

Если бы всё было так просто. С одной стороны, Кнут прав. С другой стороны — быстрая программа состоит из быстрых кусочков. Если программу писали опытные разработчики, но не думали о производительности постоянно, то получится алгоритмически оптимальная, но равномерно медленная программа — без каких-то видимых медленных участков кода.


Я не про микрооптимизации, если что. 95% быстродействия приложения — это алгоритмы и архитектурные решения (ЯП, используемые библиотеки, сетевые протоколы, подход к проектированию БД...).

Осталось договорится о метрике производительности и критерии определения улучшения этой самой производительности.
Ну и сам тезис «быстрая программа состоит из быстрых кусочков» тоже спорный, сумма из быстрых может быть как быстрее, так и медленнее суммы из медленных.

Наблюдение: переход с питона на Rust вызвал "зуд оптимизации". На питоне всё было понятно — пока оно "достаточно быстро" работает, дальше нет смысла бороться за ускорение. Всё равно супербыстро не получится.


А вот на Rust всё не так, и есть постоянный зуд делать "супербыстро". Какие dyn? Вы что, это же дополнительный lookup? Не-не-не, Box это медленно, давайте лучше на стеке. Какой Arc, вы что? Давайте лучше исхитримся без Arc!


Это как педаль газа в спорткаре. Пока едешь на машине с двигателем 0.25 литра, газуешь ровно настолько, насколько нужно, чтобы ехать. А на спорткаре хочется почувствовать скорость (ускорение!).

У меня то же самое было на Go. Ну вот не понравилось мне работать со слайсами, добавлять и удалять из середины. А если размер меняется, это же аллокация (ужас-ужас!). В итоге дело дошло до самопальной in-memory db на AVL деревьях с индексами и собственной реализацией кучи. Остановился вовремя, в ассемблерные оптимизации не полез. По бенчмаркам прирост >50%, я доволен.
Производительность в те времена, когда Кнут писал свою книгу, это совсем не то, что производительность сейчас. Кнут имел в виду производительность алгоритмов, мелкие оптимизации и тому подобную гадость, оптимизация которой занимает кучу времени, и может легко привести к нечитабельности кода. Производительность сейчас — это выбор правильного фреймворка, выбор правильных библиотек, и соблюдение культуры программирования. Это обычно не требует дополнительных ресурсов (а нередко даже экономит их), но этому так или иначе надо уделить внимание, чего многие не делают, к сожалению.

А вот вам запрещённая цитата из этой самой статьи, про этот самый кусок кода. Эту цитату, почему-то, обычно не цитируют.


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 процентное улучшение, легко получаемое, никогда не сочтут незначительным; и я думаю, что та же точка зрения должна преобладать в разработке программного обеспечения.

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

Живите с этим )))

practiced by pennywise-and-pound-foolish programmers

Из-за потерянного дефиса в устойчивом выражении оно приобрело совсем уж удручающий смысл))

эффективный код без использования GOTO. Сейчас это кажется самоочевидным.


Сейчас просто используют Exception и try catch, что по сути является GOTO в современном мире, так что это не есть самоочевидным :)

Это не является GOTO. С тем же успехом можно записать break и return как GOTO. Главное отличие — GOTO требует указание точного места, куда передать управление. Ни исключение, ни конструкции типа return этого не требуют.

После 40+ лет программирования, я перестал с этим заморачиваться. Изредка нужен даже GOTO, кусочек кода:
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? Явно мой код читать проще. У Вас код больше на 2 оператора, все равно присутствует метка перехода и я не понимаю на каком языке это написано.

Похоже на Rust. А какие 2 лишних оператора вы в этом коде углядели?

Break и loop естественно. И я пишу так как легче писать и понимать мне, а не не Вам. Для меня, в данном контексте, эти ключевые слова
однозначно лишние.

А чем отличается "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 только в другом синтаксическом сахаре.


По поводу читать легче это очень субьективно, и не одно поколение ломает по этому поводу копья ;-)

В расте просто нет go-to вот они и извращаются. Недоделанный язык.
UFO just landed and posted this here
Он вообще очень редко нужен. Мне за 40+ лет программирования понадобился всего несколько раз, когда с ним код был понятнее.

Я не занимаюсь настолько низкоуровневыми вещами, так что заранее прошу прощения если упускаю какие-то детали (меня смутил момент про тайминги в однопоточном коде) или мои эстетические чувства не совпадают с вашими. Возможно, это связано с тем, что я не видел 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 указал на неё четко и ясно. Вы же решили меня оскорбить сославшись на абсолютно корректное утверждение. Также прошу заметить, что указанный участок кода не имеет никакого отношения к обсуждаемой теме, так зачем сейчас весь этот разговор?

Ну Вас он не оскорблял. Он написал, что Вы сказали глупость, а это не оскорбление. Глупость может сказать и умный человек. Вам просто не хватает опыта в данной области. Работа с портом ввода вывода в реальном времени довольно специфична. Ну и я недостаточно документировал, написание ещё в процессе и не окончательно хотя уже работает. Это первая софт (bitband ) реализация (насколько я знаю в мире ) усб хоста на битах порта. И с кодом этой функции тут все в порядке и по функциональности и по стилю. Это как раз тот редчайший случай когда с goto читать проще.

Отлично, я рад, что мы вернулись к теме обсуждения. Давайте теперь предметно.
Изначальное утверждение, с которого началась эта ветка: читать с 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 — это только частный пример того, о чём говорится в обсуждаемой статье. У каждого кода есть причина по которой он написан именно таким образом, каким написан. Тот факт, что ваш код непонятен другим людям говорит о том, что при его написании для вас более приоритетно было что-то другое. Если это достаточно важно — ок, если нет — вы просто один из тех инженеров, о которых говорит автор. Это можете знать только вы.
На сим, заканчиваю этот разговор. Желаю вам успехов с вашей первой в мире реализацией и благодарю за минусы в карму. Доброй ночи

Не надо ничего редактировать. READ_BOTH_PINS читает оба бита DP/DM USB в верхний (старший ) байт. Все остальные биты кроме двух интересующих — замаскированы 0. В STORE[] — пишется комбинация, где младший байт — время (клок/8) и старший байт — состояние шины. Если оба бита шины в 0-- то выйти. Дальше ждем TOUT изменения состояния шины. Если не изменилась — то выходим. Если изменилась — то читаем еще раз ее состояние и пишем в STORE.
В идеале — биты меняются синхронно (один вход — это инверсия второго) кроме конца пакета, когда оба в 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 позволяет без траты времени на поиск метки понять что происходит.
Кто вам сказал, или с чего вы взяли, что время измеряется итерациями цикла? Время берется с clock counter процессора «rsr %0,ccount» для xtensa. В случае STM32 просто с счётчика таймера. Тут важно зафиксировать, что в момент времени Х шина была в состоянии У.Более того, резолюция времени подобрана таким образом, чтобы 6 единиц подряд умещались в 256 тиков. А выход с тмоут — признак что на шине просто нет ответного пакета и его точный тайминг просто не важен, важно выйти хоть когда нибудь, в примерном интервале времени

Чисто позанудствовать


Тем не менее, рассмотренные примеры говорили, что 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

что можно примерно перевести как


Я бы мог еще больше улучшить программу путем "удвоения" цикла, так что ее ее машинный код [после компиляции — прим. переводчика] будет выглядеть как-то так
Sign up to leave a comment.

Articles