Pull to refresh

Comments 57

Да в C коде его постоянно используют за неимением альтернатив
А какие альтернативы имеются в виду? Я считал, что код на C99 (и, возможно на более раннем, с потерей производительности) с goto — можно переписать структурно…
Ну традиционный пример: выход из глубоко вложенных циклов и условий (актуально, скажем, при отсутствии исключений в качестве альтернативы).
Переписать структурно-то можно, но код будет медленнее и дико запутанным.
Полностью согласен, сам считал goto плохим стилем до того момента, пока не начал программировать на C. Иногда с его помощью получается куда более читабельный код. Да и не зря некоторые называют C макроассемблером.

Но в моем мире на C++, например, уже распространяется правило, что goto это не есть хорошо, хотя бы потому, что там RAII. Да и вообще больше возможностей написать код лучше.
Ну традиционный пример: выход из глубоко вложенных циклов и условий

Циклы заворачиваются в функцию; вместо goto вызывается return.
Параметров передаётся при этом 20, конечно, но зато return вместо goto.
Как говорится у функции может быть 0 параметров, 1 параметров, 2 параметра, 3 параметра, слишком много параметров. Если имеется 20 параметров, то вы явно делаете что-то не так. все это заворачивается в структуру и передается одна структура.
Угу. Заворачиваем всё в объекты (или эмулируем их структурами). Иначе говоря, уходим всё выше и выше, объём кода необходимого для поддержания «структурности» всё больше и больше. На выходе — Java, когда на одну строчку кода может приходиться до 40 строк всего остального.
Оттуда и появляются IDE, которые генерят эти самые 40 строк…
Структуру передать ничем не медленнее, чем 2 параметра. И да, goto не поругается только на уровне ассемблера. С уже достаточно высокоуровневый язык, чтобы на нем писать читабельные программы, не теряющие от этого в скорости…
Сложно сказать, что читабельнее — goto cleanup, или функция на каждый уровень вложения объектов

int main()
{
  context ctx;
  foo* = createFoo();
  do_something_with_foo(&ctx, foo);
  free(foo);
}

void do_something_with_foo(context* ctx, foo* foo)
{
  if (foo_is_bad(foo)) return;
  char* bar = malloc(100);
  ctx.foo = foo;
  do_something_with_bar(ctx, bar);
  free(bar);
}

void do_something_with_bar(context* ctx, bar* bar)
{
  for (int i = 0; i < 100; i++) if (rand() & 3) return;
  printf("bar is a winner!\n");
  win(ctx.foo, bar);
}

Хм… На тему аппаратных циклов — DJNZ у 8051, LOOP у 80x86, REP* префисы у 80x86.
А еще REPEAT и DO на архитектуре PIC24 и dsPIC33. Удобство написания циклов этими инструкциями не повышается, но зато не тратятся такты на исполнение заголовка цикла! Тем самым отпадает необходимость в «разматывании циклов» там, где требуется высокое быстродействие.
Как всегда — на уровне машинного кода жертвуем удобством ради скорости :-).
Про DJNZ и LOOP. Логика у них одинаковая: уменьшить регистр на единицу и, если он не ноль, прыгнуть по смещению, Т.е. это всё-таки «условие плюс goto». Это не привносит структуры в программу на машинном коде. Задача слежения за ней всё ещё остаётся на плечах программиста. Я всё ещё могу написать так:

label1:

label2:

loop label1:

loop label2:

Т.е. «циклы» будут пересекаться. Никакой структуры.

Про REP*. Так как префиксы позволяют зациклить только единственную инструкцию, то они не создают свойства композиционности (возможность цикла в цикле). Вообще строковые команды — это некий архаизм, а на REP префиксы ныне активно навешивают новые смыслы (например, для HLE они превратились в XACQUIRE и XRELEASE).

Мне кажется, что, если бы язык Brainfuck имел аппаратную реализацию, то инструкции, соответствующие его операторам [ и ], как раз служили бы задаче обеспечения циклов со структурой на машинном уровне.
Кстати, еще у TriCore очень вкусные реализации циклов:
Four special versions of conditional jump instructions are intended for efficient
implementation of loops: JNEI, JNED, LOOP and LOOPU. These are described in this
section.

Если говорить про языки — то у Forth вроде были очень приятные циклы, и они есть в аппаратной реализации.

На тему пересечения циклов… На одних repeat {} until(true); + break можно нагородить кашу не меньше, чем из JMPов.

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

Forth — это вообще для меня яркий просвет во тьме недоразумений машинных языков. Элегантный и мощный.
Жаль, что его возможностей по эффективной реализации недостаточно для топа.
Зато он идеален для многих встраиваемых решений.

Жаль, не взлетел.
Автор писал про циклы с пред-условием (for или while), а эти примеры — с пост-условием (repeat...until).
собственно, CJNE у 8051 прекрасно для пред-условий подходят:
CLEARBUF:
; for(
MOV R0, #BufBegin; void *i=buf;
LoopFor:
CJNE R0, #10, LoopBody; i<=buf+10;
RET
LoopBody:
MOV @R0, #0; *buf =0;
INC R0; i++)
SJMP LoopFor

Впрочем, я согласен, что структурированность уже не очень, исходный посыл последовательности _НАПИСАНИЯ_ действий разрушен. а вот последовательность ИСПОЛНЕНИЯ — как раз чиста и пыщпыщна.
Спасибо за книгу! Вот ещё пара отличных:

Ссылки с сайтов издателей, цены там завышены. Обычно можно найти дисконт или магазин, где подешевле.

На lektorium есть курс по параллельному программированию от Евгения Калишенко, там тоже объясняется, почему использовать потоки напрямую сложно и редко нужно.
Офигенный курс! Спасибо за ссылку.
Спасибо за ссылку, посмотрю!
Между новыми языками, изначально пытающимися поддержать параллелизм
А вот в Go всё относительно просто Go Parallel, Go Parallel 2, Go Parallel 3. Кроме того, в Go встроен гугловский Thread Sanitizer Go Race Detector, который отлавливает большинство ошибок, каких мог наляпать начинающий любитель goto и обращений к одним участкам памяти из разных потоков.
Плюсую. А ещё, когда привыкаете к подходу Go share memory by communicating, его же можно применять и на других языках, и ваши многонитевые алгоритмы становятся мягкими и шелковистыми.
Есть некоторая ирония в том, что в Go не только есть goto, но и иногда через него проще всего реализовать такие структурные блоки, как например в перл*:
for {
...
} continue {
...
}
* блок continue выполняется после каждой итерации for, даже если она была прервана посредине оператором next, очень удобно для циклов где возможно прерывание итерации (но не цикла) вследствии ошибки
В Go goto имеет меньше прав, чем в C, с goto нельзя прыгнуть за пределы variable scope, поэтому с ним наговнокодить гораздо сложнее.
Вот начитаются такого программисты и пишут потом bruteforce в один тред! :-D

На самом деле НЕ ВСЕ задачи порождают излишнюю сложность при многопотоковости, не во всех задачах нужна синхронизация сложнее bool stop = false и т.д. И не во всех задачах нужен параллелизм, IMHO впихивать его туда где он не нужен — примерно такое же зло.
При разработке на си — goto вполне себе стандартный паттерн нетривиального досрочного завершения функций. А высказывание в заголовке — бред, т. к. оператор языка и механизм операционной системы сравнивать не корректно.
goto -> циклы довольно простой переход. В большинстве случаев программисту нужен не goto а именно цикл, поэтому такой переход очевидный и в большинстве случаев всё сильно упрощает.
Ручное управление потоками и синхранизацией нельзя заменить на единый и подходящий в 99% более простой механизм. Существует огромное количество паттернов многопоточности и в зависимости от задачи может быть необходимо как простое распараллеливание циклов, так и многопоточная очередь, producer->consumer и т. п. А ещё из всего этого нужно как-то работать с ассинхронным вводом / выводом.
| себе стандартный паттерн нетривиального досрочного завершения функций.
return?
Во время работы функции инициализируются разного рода структуры другими функциями. и в конце функции есть метка (:fail или :error) в которой происходит очистка этих структур в случае сбоя. Был бы это С++ — всё сделали бы деструкторы. А в C приходилось так вот заморачиваться, чтобы не повторять нарастающий, как снежный ком, список функций освобождения ресурсов после вызова каждой функции, в которой что-то может пойти не так.
Поторопился я выше писать, Вы уже изложили мою идею =\
Сишный goto-defensive-programming во всей красе проявляется в модулях ядра, когда в процессе создания устройства (например) что-то может отвалиться и в таком случае нужно откатить все внесённые изменения, но не более, хвосты у функций инициализации обычно выглядят так:
	out_devcreate: destroy_created_devices(created_num, poums_class);
	out_devinit: deinit_poums_devices(init_num);
	out_class: class_destroy(poums_class);
	out_reg: unregister_chrdev_region(first, num);

	return err;

И в зависимости от того, на каком этапе произошла ошибка, в ту часть этого хвоста и попадаем.
UFO just landed and posted this here
Про транзакционную память и её «обещания об избавлении» я также хочу как-нибудь написать статью для Хабра. Пока что есть серия постов на IDZ про её аппаратные воплощения: 1, 2, 3, 4.
Я бы воздержался от категоричных высказываний по поводу потоков. Есть 2 типа приложений, надо которыми я работаю.
Первый тип занимается обработкой данных и должен решить задачу как можно быстрее. Соответственно нужно загрузить все ядра и параллельно выполнить на них работу. Как это сделать? Без потоков никак.
Второй тип, это обычное десктоп приложение. В нем, в главном потоке идет обработка run-loop и любая сколько-нибудь тяжелая задача в главном потоке скажется на отзывчивости приложения. Как бороться? Сделать отдельный пул рабочих потоков для фоновых задач, в которых доставать задачи из очереди и обрабатывать.
т.е. решений достаточно много и нельзя говорить, что «потоки — зло». Да, надо думать о синхронизации доступа к ресурсам, делать доступ к общим ресурсам в одном и том же порядке, может даже использовать новомодные lock-free структуры. Но страшного или плохого в этом ничего нету.
Автор не говорит, что потоки это плохо. Он говорит, что это слишком низкоуровневый инструмент (также как goto), и призывает использовать более высокоуровневые абстракции, а именно (поскольку пост в блоге Интел) что-то вроде Intel Thread Building Blocks (TBB) и Intel Cilk+.
Ну, это очень зависит от языка. В Си это так. На высоком/интерпретируемом уровне, например, на том же Python, потоки + Exceptions + threading.Event порождают довольно читаемый и производительный код.
Статья, вообще-то, не про отказ от потоков, а про необходимость закрытия их дополнительными уровнями абстракции.
Мне кажется что вы не поняли про что статья, в ней же не предлагается всем писать однопоточные приложения. Как я понял речь идет о том чтобы заменить в программах низкоуровневые абстрации (создать поток, синхронизовать ресурс) на другие более высокоуровневые абстракции.
Полностью поддерживаю. Думаю, что содержимое статьи в большей степени применимо к случаям перемалывания данных. Случай же структуризации всего приложения не освещен.
Эппл вместо потоков предлагает свой Grand Central Dispatch. Жаль только, что его толком не портировали платформы, отличные от их OSX/iOS.
Я хоть с экосистемой Apple по жизни почти не сталкиваюсь, тоже часто жалею о том, что их GCD не взлетел кроссплатформенно. С другой стороны, Apple протолкнуло OpenCL, за что им спасибо.
зашёл в тред чтобы написать про GCD. Все проблемы, описанные в топике прекрасно им решаются. Вся балансировка и абстрагирование возлагается именно на него, а программист уже пишет вполне понятный код.
А то что не портировали — это да, несмотря на открытость кода, как то не спешат взять этот прекрасный инструмент
Более того, я не припомню ассемблера, в котором аппаратно сделан оператор for или while.

А я могу вспомнить и даже не одну реализацию на различных архитектурах. Используется обычно мнемоника loop. Причем есть loop'ы как по аппаратному счетчику (for-like), так и по условию (while-like).

Код может выглядеть примерно так:
    loop    15, endloop1
    nop
    nop
    nop
endloop1:
    nop
Я попробовал объяснить это своё высказывание на примере с loop здесь:
habrahabr.ru/company/intel/blog/206030/#comment_7532679
Т.е. дело не в автоматическом счёте операций, а «скобках», создающих структуру.
Всего лишь запутанные нити шафрана. Но аналогия с логотипом хороша :-)
Нам преподаватель по ЯП в универе как то выдал — «только алкоголики боятся пить в одиночку», когда ему сделали замечание по поводу использованного goto в коде.
Да, полностью поддерживаю, как только я узнал что в хаскеле ввод-вывод по умолчанию неблокирующий и платформа сама порождает нечто типа green threads я понял что потоки и всякая явная асинхрощина (особенно на коллбеках, типа nodejs), это что-то вроде разработки на ассемблере — низкий уровнь, а значит низкая эффективность программиста
На правах редактора замечу, что первую проблему (названную «отсутствие композиции») решает нормальная документация, а как раз OpenMP решает вторую проблему " необязательности параллелизма".
И еще — вот очень интересная штука www.hoopoesnest.com/cstripes/cstripes-sketch.htm
На каждый файл — свой поток, который открывает файл, читает его, считает его, и пишет результат в другой файл.
И — никакой синхронизации!

И время выполнения расчета в десятки и сотни раз превышающее время выполнения с разбиением на традиционные два потока: читающий/пишущий и считающий.
Пост ни о чем, вроде вначале собирались, что-то рассказать — но так ничего и не рассказали.
Вот буквально недавно размышлял о том, что хвостовая рекурсия в Erlang это по сути дела цикл с условным выходом из него (по типу goto). В структурированных языках мы от этого стремимся избавиться, но иногда такие решения всетаки нужны. А рекурсивный вызов всегда будет выращивать стек. Таким образом, получается, что хвостовая рекурсия позволяет пользоваться преимуществами производительности с операциями goto и в то же время обладает понятным синтаксисом.
Sign up to leave a comment.