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

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

Компилятор делает более оптимально, минимизируя циклы обращения к RAM.

Статья о удобочитаемости, а не о эффективности кода.


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


Но опять – статья о читаемости и понятности, а не о производительности.

Кэш не регистр, быстрее быть не может априори.

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

Тут, думаю, будут удобнее макрокоманды ассемблерные а не чистый код.

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

Си же код записи алгоритма более подходит для «обобщённого» понимания записи алгоритма вне прямого акцентирования на потоке команд управления данными.

Как то так. 😋

Честно говоря, я сомневаюсь, что на языке высокого уровня можно написать это более компактно и понятно.

Компактно - запросто. С "понятно" все сложнее, потому что это чисто субъективное понятие, которое полностью зависит от бэкграунда читающего. Например я чаще имею дело с армовским ассемблером и меня вымораживают названия регистров в x86.

Автор написал код на ассемблере, транслировал его в плюсы и получил нечитабельное нечто - вот так новость, видимо ассемблер самый понятный в мире язык. Вот, ловите нормальный код:

while ( !p->next && p->parent ) p = p->parent;
p = p->next ? p->next : p;
while ( p->f_child ) p = p->f_child;

Ваш код не работает корректно если коренных узлов больше одного. То есть не эквивалентен C++ коду в статье.

Даже если не вариант сделать список корней кольцевым, то можно так:

while ( !p->next && p->parent ) p = p->parent;
if ( p->next ) p = p->next;
else
{
    while ( p->prev ) p = p->prev;
}
while ( p->f_child ) p = p->f_child;

Да, теперь все в порядке.


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


Теперь надо только разобраться кто из них более удобочитаемый. :)

Этого не может быть. Нормальный компилятор как минимум заиспользует инструкцию test вместо cmp.

Удобочитаемый - тот, где путь выполнения выражен простыми для понимания конструкциями вроде цикла while. А не jmp и иди разбирайся куда оно там прыгает.

Нет, вы не поняли. Я имел ввиду что ваш C++ код и мой C++ код, компилируются одинаково. Хотя и отличаются в исходниках. И они компилируются до варианта в статье – с test и использованием второго регистра (код спрятан в спойлере «Конечно, я скомпилировал этот C++ код».

Если вам все еще интересна тема...

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

Я много и успешно(!) писал на разных ассемблерах в свое время, но, тем не менее, демонстрация кода на ассемблере вызывает у меня совершенно ненужное напряжение, по сути, расстраивает меня, потому что вы написали код на ассемблере который используется у вас(!), а мне чтобы использовать ваш код нужно будет переписать его на ассемблер, который поддерживает моя система- мой процессор, а он может быть 16-битный или 8-битный, или даже 64-х битный.

То есть причину очень просто понять если вы писали код не только на ассемблере для 32-битных процессоров, а еще для, например 8-битных процессоров, или хотя бы для 16-битных процессоров.

Удивительно что вам никто не привел этот аргумент! Удивительно как коротка память у целого сообщества, ведь язык С появился именно по этой причине: чтобы сделать код читаемым для людей которые работают на разных ассемблерах, на разном железе. Кстати, язык С появился, когда процессоры были в основном 8-битные!

Кстати, я не вижу признаков плюсов в вашем коде, это вполне плейн-С код.

По-моему, про то что язык С, а за ним С++ обладают преимуществом для понимания кода написанного на них перед любым ассемблером можно прочитать где-то в начале изложения основ в их стандартах, конечно при желании обращать внимание на эти основы. Собственно, все дело в том, что С/С++ это универсальный язык в отличие от любого конкретного ассемблера – универсального ассемблера не существует!

В этом смысле ваш вопрос является достаточно интересной провокацией, которая демонстрирует этот достаточно интересный эффект короткой памяти сообщества!

Я попробовал написать аналогичный код на kotlin (работоспособность не проверял, он больше для иллюстрации). Благодаря elvis оператору, nullable типам и хвостовой рекурсии выглядит сильно иначе. На мой взгляд, в таком декларативном стиле получается понятнее.

tailrec fun lastFChild(n: Node): Node =
n.fChild?.let{ lastFChild(it) } ?: n

fun Node.first(): Node =
prev?.first() ?: this

fun cycle(n: Node): Node =
n.next ?: n.parent?.let{ cycle(it) } ?: n.first()

fun Node.findNext(): Node =
lastFChild(cycle())

Кажется, если написать на С с использованием goto получится тоже самое, что на Асм?

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

В общем любопытно, лайкнул бы, да грехи карма не позволяет )

На ассемблер можно и нужно писать сообразно идеи структурного программирования. Структурное программирование далеко не заключается в неиспользованием goto/jmp.

Да, но при этом использование команды перехода не помогает эту структуру легко выявлять. И даже c использованием каких то отступов.

Для примера в Форт при близкой семантике представления кода в сравнении с ассемблером всё же есть:

и IF… ELSE… THEN
и BEGIN… WHILE… REPEAT
и BEGIN… UNTIL
и DO… LOOP
и CASE… OF… ENDOF… ENDCASE

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

FOR… NEXT SWITCH BREAK CONTINUE…

P.S. Даже на стандартном ассемблере для AVR примерно такое тоже возможно Конструкции структурного программирования для AT90s8515

Вы просто путаете форму с содержанием. Все эти if-else-while, суть высокоуровневые конструкции, которым не место в ассемблере.


Но суть структурного программирования совсем в другом. Можно программировать структурно и на ассемблере. А можно писать неструктурированный код и на C++.

А, такой вариант облегчающий ассемблерное программирование? Дополнительные команды ассемблера2

Из этого проекта для AVR

И статья Машина времени для крошек.

P.S. В любом случае при использовании в большой программе ассемблера программист старается формализовать в какой то степени и возможности по его эффективному использованию с точки зрения своих трудозатрат на написание кода на нём и дальнейшего его понимания.

У меня достаточно личного опыта использования ассемблера в программировании на них, чтобы сформировался определённый взгляд на его счёт. 😋

Даже когда делал эмулятор PDP-11 процессора на x86 ассемблере и то от связки его с Форт не отказался.
Вы просто путаете форму с содержанием.

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

Сложно, не используя комманды структурного перехода, а GOTO остаться в рамках «ограничений» структурного программирования.
В этом аспекте рассмотрения можно сказать, что и Дракон язык схем сделан «структурным» при ограничениях его применения.

P.S. Кстати, по возможностям изменения эргономики мнемоник ассемблерных команд показателен случай из программы AB (Algorithm Builder 5.44) для AVR Описание ассемблер мнемомик AB программы
Учебник по работе с АВ

Топик на форуме vrtp.ru пользователей данной программы:
Algorithm Builder for AVR, Начинаем

Сам автор программы, не найдя ей коммерческое развитие сделал её бесплатной и не стал продолжать развитие идеи в рамках ARM архитектуры.
Но суть структурного программирования совсем в другом. Можно программировать структурно и на ассемблере
Из текста программы будет сложно извлечь структуру. Ведь структурное программирование — это использование вложенных друг в друга блоков типа «последовательность», «ветвление», «итерация». Языки высокого уровня эти блоки естественно отображают отступами, а на ассемблере границы и вложенность блоков никак нельзя понять, не читая весь код от начала до конца.

Ассемблер читается очень и очень не так как ЯВУ. Отступы конечно можно использовать, некоторые так и делают. Но лучше всё-таки читать по ассемблерному – по вертикали и аккордами, а не отдельными инструкциями. Тогда и вложенность отличается прекрасно.

Например, по условному переходу не видно даже, вверх он идёт или вниз.
А если вниз, что это за структура — ветвление (переход на else) или итерация (переход на конец цикла как часть условия while).

Очень даже хорошо видно, если ограничить переходы в пределах страницы текста. А это кстати одна из хороших практик в ассемблере. Если надо прыгать дальше, значит нужно написать по другому.


Правильные имена меток тоже помогают лучше проследить структуру.

Тогда предложенный код можно на С написать так же как на АСМ и не факт, что будет менее понятно.

Короче для меня пока тезис не доказан :-)

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

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

названия переменных фиксированы (эах, оух, ...)
Это скорее издержки IDE, которые для ассемблера от примитивного текстового редактора ничем не отличаются. В Visual Studio для MASM даже подсветки синтаксиса нет! Не говоря уже о сворачивании частей кода (аналог #region в шарпе). Я писал для этого расширение, которое в том числе позволяло привязывать к регистрам вменяемые имена (в виде всплывающих подсказок), и в идеале их вполне возможно отображать непосредственно в листинге.
Тот факт, что у вас названия переменных фиксированы

Не надо рассуждать о регистрах как о переменных. Они такими не являются. Переменные в ассемблере, находятся в памятью и имеют нормальные имена. А регистры, это регистры – ресурс которого просто нет в высокоуровневых языков.

А регистры, это регистры – ресурс которого просто нет в высокоуровневых языков.

Изначально предполагалось, что в Си будет к ним определённый доступ по ключевому слову их расположения register, но это так и осталось только задекларированной, но не поддержанной возможностью в Си компиляторах, а также могли бы быть добавлены команды непосредственно присутствующие в процессоре, как теже сдвиги по непосредственной работе с ними.
И язык Си по части своих возможостей стал бы похожим на существующий и сейчас проект С-- языка. 😋

P.S. И непосредственного доступа к стеку процессора нет в высокоуровневом языке, если не ввести, как в Форт, непосредственно доступ к управлению им и дополнительного стека оперирования данными вместо необходимости использования непосредственных регистров процессора в явной форме.

В Turbo C register вполне размещал переменную в регистр.

Но если переменная использовалась не как регистровая, например брался её адрес, то она размещалась в памяти.

Тут речь о том, что ключевое слово register игнорируется компиляторами. Вы не проверяли, какой код сгенерирует Turbo C без слова register? Все обычные компиляторы так же по возможности поместят переменную в регистр. То есть, что есть это слово, что нет его, на кодогенерацию никак не влияет.

Без register однозначно всё в стеке лежит, проверял.

Проблема в регистровой переменной -- невозможность получения указателя. Компилятор это вычисляет и вне зависимости от наличия register кладёт переменную в память.

Современные компиляторы register не используют, это да.

То есть, Turbo C вообще не умел оптимизировать? Если переменная не помечена явно словом register, он её обязательно размещал в стеке. Очень слабый компилятор, получается.
То есть, для повышения читаемости вы предлагаете писать код в стиле gcc -O0, если надо изменить переменную, надо её прочитать из памяти (используя символьную метку, чтобы видеть название переменной), модифицировать, записать в память. А больше чем на 1 операцию держать в регистрах не рекомендуется, чтобы человек, читающий код, не напрягал себя запоминанием соответствий регистров и переменных.

Конечно нет. Я такое не писал. Опять повторяю, регистры (да и стек тоже) это ресурс, которого нет в языках высокого уровня. И поэтому нельзя рассматривать их с высокоуровневой точки зрения. Ничего не получится.


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

Тогда вернёмся на сообщение выше. В computer science, в алгоритмах, есть такая абстракция, как переменная. Она соответствует регистру, или нет? Если да, как узнать, какому регистру соответствует какая переменная в некотором участке кода?
Она соответствует регистру, или нет?

Нет.

Вы же писали
Переменные в ассемблере, находятся в памятью и имеют нормальные имена
Как в соответствии с этим реализовать, например, алгоритм Евклида с переменными a и b
while (a != b)
{
    if (a > b)
        a = a - b
    else
        b = b - a;
}
Чему в ассемблере соответствует переменная a? Регистру, адресу в памяти?
Как в соответствии с этим реализовать, например, алгоритм Евклида с переменными a и b

А зачем нужно это ограничение «с переменными a и b»? Правильный вопрос будет «Как реализовать алгоритм Евклида?»

Когда вы будете обдумывать реализацию алгоритма, или объяснять его кому-то, вы как будете называть объекты «a» и «b»?

Любопытно, а как Эвклид представлял переменные и поток команд в своем алгоритме.

Может не переменные, а парметры/неизвестные:

gcd(A,A)->A;
gcd(A,B)when A>B ->gcd(A-B,B);
gcd(A,B)->gcd(B,A).

Не холивара ради, но вот такой вопрос: а для arm-процессоров будете повторно такой код писать? Поскольку набор команд и подходы написания кода для arm совсем другие. Или же ОС-независимый GUI не включает ОС под arm?

К чему это я – если вам нравится хардкор и оттачивание до мелочей, не эффективней ли писать на C (т.е. даже не плюсах)?

Не холивара ради, но вот такой вопрос: а для arm-процессоров будете повторно такой код писать?

Да, я считаю, что для разных процессоров код надо писать отдельно. Да, «по мотивам», но отдельно.

Вот сейчас создателям UNIX было обидно.

НЛО прилетело и опубликовало эту надпись здесь

Ну-у-у, а зачем вообще рекурсивно??? Нерекурсивный алгоритм в случае намного и читабельнее и компактнее.

НЛО прилетело и опубликовало эту надпись здесь
Это удивительно. Две рекурсивные функции компилятор сложил в одну большую нерекурсивную.

Это он еще fork-join не применил

Автор, можно изобразить на ассемблере любое crud spring boot rest приложение с секьюрити, жпа и всем полагающимся. А самое главное узнать насколько читаемо это будет выглядеть и сколько времени займет. Любой джава джун за час запилит.

Или нарисуйте фронт к нему, который джун js так же быстро изобразит.

Каждый язык, - для своих целей хорош. Не надо ругать одни и превозносить другие.

Хотя, я вроде и не совсем понимаю что, все эти «crud-spring-boot-rest» значат, но если про веб, то мы это проходили. Оно все еще работает.


На секюрити не жалуюсь. Уже 2 года форум работает вообще без инцидентов и без всякой поддержки и администрации.


Удобочитаемость кода тоже ничего. По крайней мере, даже люди далеки от ассемблера могут ориентироваться и понять что к чему.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории