Pull to refresh

Comments 46

Ждём статью «goto в спортивном программировании» :)
спортивное как раз хорошо попадает под прикладное:

там важна общая реализация алгоритма,

а ограничения по ресурсам ставятся такие, что их выполнение/невыполнение зависит только от алгоритма, а не конкретных инструкций в его реализации.
Не скажите — иногда один и тот же код с vector'ами не проходит, а с массивами получает AC.
Хотя goto, разумеется, таких «оптимизаций» не даст.
да, STL — такой STL :)

не учел, так как когда занимался олимпиадным программированием, задачи решал на Паскале, хотя как основной язык изучал С++ — для меня тогда было критично не наступать на грабли C/C++ — кстати, мои более продвинутые товарищи так же делали :)
Чаще всего просто не нужно (зато break и continue юзается десятками раз в каждом исходнике).
Единственный пресловутый случай, когда мне goto на олимпиаде помогало — это тот самый выход из нескольких циклов.
Задачка:
Есть страшный расчет, который надо проводить тысячи раз. В каждом есть10 подзадач, каждая занимает по пол часа. (Реально есть)
В качестве вычислителя есть только ноутбук 2 ядерных по 1.6ггц. Ни каких других вычислительных мощьностей ближайшие пару месяцев не предвидится, а посчитать надо.
Держать ноутбук не выключая более 2 часов не возможно.

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

Скажите лучшее решение, чем без goto, тк мне самому оно не нравится ибо велосипед.
Я серьезно!

Мое решение, но пока оно только сделано не используется.

Я реализовал паттерн- моменто(я бы назвал его скорее дампом).
При поступлении события, все память складывается в xml, вместе со всеми значениями счетчиков циклов и временными переменными. Затем, чтобы возобновить процесс вызывается точная копия метода, где происходил процесс с несколькими отличиями, во первых он получает в качестве параметра номер метки goto на которую надо перейти, чтобы начать процесс с той же точки, что и он был прерван. Далее идет инициализация всех переменных из дампа, и через маленький switch по тому номеру метки идем в нужное место циклов. В итоге расчет начинается с той же точки на которой он был прерван.

Чем плохо- есть полный дубляж счетного метода, есть goto.
Какое лучшее решении можно сделать? Я на полном серьезе спрашиваю, если кто предложит ответ- буду рад как кот сметане.

Язык C#(переписывать на С++ не предлагать, это на долго и в отладке я погрязну еще на долго.)

Действительно страшно. На какой глубине находится самая глубокая точка сохранения?
Можно предложить:
— реализовать методы, которые будут выполнять завершение внутренних циклов, возвращаться и передавать управление методам для более внешних циклов (плохо, потому что код умножается во много раз)
— пропускать уже вычисленные части циклов с помощью if() (которые будут срабатывать только при первом входе)
— описать состояние процесса в самой глубокой точке сохранения и реализовать всю программу как один цикл, реализующий переход от такого состояния к следующему.

Я бы, наверное, стал делать так. Тело каждого цикла выделил бы в функцию, которая что-то бы делала, а дойдя до внутреннего цикла, вызывала бы в нем функцию следующего уровня. У функции — параметры, позволяющие описать досрочный выход и продолжение счета (блок с копией локальных переменных?). Но это тоже громоздко.
Вот ваш вариант мне вполне себе нравится, но тогда придется либо кучу(порядка 15, не хочу вызовы превращать в winapi стиль) параметров передавать в функцию и все по ссылку ref, либо на плодить глобальных переменных.

Реализовывать все как 1 цикл- я вас не понял.

Вариант с if действительно выглядит элегантно относительно.
Получится что есть 3 переменных булевских, которые говорят работал ли цикл в момент отключения или нет. И потом if(var1) то заходим в блок. далее if(var2) заходим, а в последнем if(var3) оказывается false, значит с этой точки и считаем. Соответственно при выходе из цикла переменную ему соответствующую ставим в false, что мы от туда вышли.

Надо попробовать сделать.
Думаю, что сохраненные переменные лучше организовать в классы, причем структура классов должна отражать структуру циклов: цикл на каком-то уровне получает инициализирующую переменную, если она null — значит это штатный вход, а если нет — сохраненный. Извлекает из нее объект для цикла следующего уровня. Тот тоже смотрит: если это не null — значит, это его сохраненные данные, а если null — сохраненных данных не было, вход штатный. По-моему, должно сложиться. Плохо, если точки входа образуют не линейный список, а дерево. Но и тогда можно разобраться.
Т.е. предлагаете наплодить 3 класса, переписать алгоритм(120), провести заново тестирование и верификацию, снизить скорость работы довольно сильно?

Пожалуй оно того не стоит.

Вариант с 3 if мне больше нравится и я его уже реализовал. Он работает. goto нет, и код практически не изменился
Вариант хороший, но я не нашел ничего подобного для C#

Как то у одного товарища моего знакомого было желание взять весь thread(а лучше объект обертка над ним из .net 4) остановить его и в файл сохранить… Чтобы потом восстановить.
По моему у него это не удалось ибо это все таки не системное программирование и вроде на C# средств таких нет.
Реализовать это можно, но понадобится довольно много муторной и нетривиальной работы с определением точек безопасной остановки и разработке механизма сериализации и десериализации данных. Сложность зависит от сложности применяемого алгоритма.
в аналогичных случаях, правда, гораздо более примитивных, поступал так:

Поток обработки UI при нажатии кнопки «пауза» просто поднимал флаг «пауза». Рабочий поток проверял этот флаг в фиксированном месте, в котором легко и удобно можно было все задампить, и так же легко можно было позднее восстановить состояние и продолжить работу — это было в конце оборота глобального цикла, когда стек минимален, все промежуточные расчеты получены.

если у Вас вложенные циклы сами по себе долго выполняются, наверное, придется сделать сложнее: так же один флаг, но проверять его на нескольких уровнях вложенности, на каждом сохранять свой кусочек стека.
Самый низкий цикл то как раз не очень долго выполняется, потеря пары минут- не критична…
Наверное так и следует делать, сохранять только в допустим самом верхнем из 3 циклов.

Но вот потеря второго цикла- это уже пол часа- а это плохо. Можно избавиться от 1 цикла и 1 goto соответственно, но еще 1 получится останется.

Полумера выходит. Избавился от 2 строк, но добавлю проверку на флаг+ ожидание выхода из нижнего цикла.

Если бы понять как во второй цикл войти и выйти без goto, то я бы так как вы сказали и сделал
А если попробовать «развернуть» 3 цикла в один:

int L1 = 0;
int L2 = 0;
int L3 = 0;
while (L3 < 2){
Console.WriteLine(«L1:{0} L2:{1} L3:{2}», L1, L2, L3);
L1++;
if (L1 == 2){
L1 = 0;
L2++;
}
if (L2 == 2){
L2 = 0;
L3++;
}
}

Оформляем этот кусок кода в виде функции, которой в параметрах передаются L1, L2, L3 и вход в нужное состояние осуществляется без GOTO.
Если бы так цикл просто раскрутился, цены бы не было вашему совету.
К сожалению алгоритм переписывать в 120 строк с динамическими определяемыми границами не очень хочется.

Я не очень представля как алгоритм с не статически задаными границами можно раскручивать стандартно.

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

Не совсем понятно, почему это так. Во внутреннем цикле обработки большого объема данных, который выполняется 10^9 и более раз, каждые несколько тактов — это лишняя секунда времени работы. И заставлять процессор работать с флагами там, где достаточно обойтись явно прописанным переходом, может оказаться не очень разумно. Лишняя переменная — это не просто ячейка памяти, это еще и регистр процессора. А их запас ограничен.
Хорошее замечание.

В таком случае нужно:

1) определить, насколько высокоуровневый код используется в этом цикле: если код выполняется на заведомо высокоуровневом языке — C#, Java, C++ с использованием продвинутого ООП (наследование, шаблоны), интерпретируемом языке и т.п., — то goto Вас не спасет: уже есть много накладных расходов, на фоне которых goto теряется.

2) если все-таки хотите попробовать оптимизировать, плясать не от пристального взгляда на исходники, а от показаний профайлера — какие узкие точки показывает он (хотя, возможно, на таких масштабах оптимизации профайлер начинает искажать показания за счет изменения исходного кода)

3) если все-таки взялись оптимизировать, и при этом попадаете в тему этой статьи — занимаетесь прикладным программированием на высокоуровневом языке — нет смысла париться о таких правилах: это значит, вы уже отказались от всех высокоуровневых концепций, и спустились на уровень ассемблера/Си, чтобы быть в состоянии контролировать такую точность оптимизации.

В общем, максимальная оптимизация всегда усложняет код, нарушая кучу рекомендаций — никуда от этого не деться, но главное — 7 раз проверить, что это имеет смысл
Со всем согласен, кроме «продвинутого ООП (наследование, шаблоны)» — во-первых шаблоны вообще не очень-то ООП, а во-вторых они сами по себе не делают код медленнее, а зачастую используются как раз для достижения обратного эффекта. Кроме того, наследование тоже само по себе ничем не грозит, т.к. это просто расширение структуры данных (плюс мизерный оверхед от конструктора-деструктора которые можно и заинлайнить). В любом случае, нано-оптимизации о которых идёт речь в случае с goto могут иметь измеримый эффект только в очень нагруженном цикле, и то не факт, что он там будет. Во всех остальных случаях имхо нужно писать так, как этого ожидают другие люди которые будут потенциально работать с этим кодом.
UFO just landed and posted this here
UFO just landed and posted this here
да, я неспроста описал свойство прикладного программирования «возможность обработки ошибок через исключения». наверное, недостаточно четко описал, поэтому подредактировал начало темы, добавив определение прикладного программирования в данном контексте. Язык C не входит в это определение — более низкоуровневый.
Да в C++ механизм обработки исключений тоже не идеален. Нет блока try/catch/finally — попробуй сделай нормально финализацию. А блок try/catch не перехватывает Win32-исключений (например, ошибка деления на ноль). В свою очерень, SEX-блок __try/__finally может не перехватывать исключения C++.

В си++ вместо этого используется RAII, и если посравнивать с граблями в том же Java которые возникают при использовании finally, то выходит что RAII лучше :)
UFO just landed and posted this here
UFO just landed and posted this here
На самом деле достаточно было написать
«В прикладном программировании критически важен один параметр кода — сопровождаемость.»

И заставить всех программистов в мире это выучить. А потом убедиться, что поняли.
Остальное — следствия.
Да, было бы достаточно :)

Только мы же люди аналитические — набиваем шишки, анализируем, выводим общие правила, потом дзен, а потом пытаемся заставить других этот дзен понять быстрее. Но заставление плохо проходит даже с теми, кто рядом. Хотя рычаги всегда есть — сводятся к «заставлению» поддерживать свой код.
Автор делает одно очень важное допущение — он считает что все используют C++.

В C есть конечно longjmp, но лишний раз его использовать тот ещё аттракцион.
Я явно написал, что это общее для всех высокоуровневых языков, поддерживающих goto — C#, Java, PHP,…
MSDN: msdn.microsoft.com/ru-ru/library/13940fs2#Y240

Кратко: в C# применение оператора goto оправдано для:

1) Перехода между case-метками внутри switch-блока;
2) Для выхода из вложенных циклов.

p.s. Иначе такие примеры не приводились бы в официальных руководствах, причём без всякого рода предостережений, верно?
Конечно, читал это во время написания (только английский вариант — этот русский перевод удивил своим «качеством»).

К сожалению, примеры насквозь кривые.

Конкретно по примерам:

1. switch — хоть и маленький, а уже непонятный пример (понять можно, но нужно включить голову и внимательность):

1) сначала хотел написать альтернативу — вешать флаг внутри switch, затем в соответствии с флагом выполнять. Но данный извращенный код вообще из пальца высосан, таких авторов надо увольнять, а код заменять на примитивный:

Hashtable pricesBySizes = new Hashtable();
pricesBySizes[smallSize] = smallSizePrice;
...

if(pricesBySizes.ContainsKey(size)
   //запишем цену
else
  //отругаем за неверный ввод


2) за magic constants 25 и 50 уже можно руки оторвать любому, независимо от фирмы. А Вы говорите «MSDN», эх(((

2. вложенный цикл: 1) создает сразу 2 метки; 2) дублирование — несколько раз вызывается Console.Writeline — как раз потому, что вместо отслеживания результата поиска применили goto.
А так было бы:

result = findEqualNumber(array, myNumber);
resultString = "The number {0} " + (result == null ? "was not" : "is") + "found";

//вывести результат 
Это автоматический перевод на русский. Примеры, как можно понять, демонстративные — демонстрируют, какой синтаксис нужно использовать для goto внутри того же switch — и я не вижу смысла где-то зараннее объявлять числа, чтобы они перестали быть волшебными, а пример от этого стал длиннее.

Может, авторитетным для вам окажется мнение 9 специалистов (Симон Робинсон, Олли Корнес...), написавших «C# для профессионалов»: «Как правило, не рекомендуется использовать оператор goto. Вообще говоря, его применение противоречит объектно-ориентированному стилю программирования. Однако в одном случае этот оператор очень удобен — в случае переходов между метками в операторе switch, в частности потому, что оператор switch в C# не позволяет „проваливаться“ сквозь метки case.»

И да, никого не надо увольнять.
Тоже подозреваю, что автоматический перевод, но при этом прямо написано — «переведен вручную».

Согласен с Вами — примеры демонстрируют именно синтаксис и семантику goto. Но никак не образец написания кода.

Безотносительно отношения к авторитетам рассмотрим приведенное Вами высказывание:
1. Не рекомендуется. 2. Противоречит ООП. 3. Удобен в switch. 4. в частности, чтобы проваливаться сквозь метки case:

3. удобен — я это понимаю как «удобен в написании»: если имеем индульгенцию использовать его в switch, то уже не будем париться, думать, как сделать по-другому — возьмем и напишем;
4. «проваливаться сквозь case»:
1) заметьте, пример MSDN даже сюда не подходит: в нем прыгают наверх — возможно, разница тонкая, но есть;
2) разве хорошо проваливаться сквозь метки только потому, что в С/С++ это было, и мы привыкли? Нет, это ошибкоопасно: сложнее заметить, где есть break, где нет — для этого и сделали обязательным break. Это, если хотите, синтаксическая соль, а мы хотим ее обойти.

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

Да, и меня смущает всё время это «переведен вручную». Но зачастую там откровенные моменты есть, которые… ну никак не мог бы допустить человек с минимальным знанием английского.

Едем дальше:

1) «Не рекомендуется» — это да. Но обратите внимание, как осторожны здесь авторы. Они говорят «Как правило, не рекомендуется» (<=> «Обычно, не рекомендуется»). Это значит, что есть исключения, и именно в конкретных исключительных ситуациях (блок switch, выход из вложенных циклов) goto оправдан и является одним из верных решений.
2) «Удобен в switch» — удобен и в написании (не нужно объявлять меток) и в использовании, но только в ситуациях, когда он там требуется.
3) "… в частности, чтобы проваливаться сквозь метки case" — «разве хорошо проваливаться сквозь метки только потому, что в С/С++ это было, и мы привыкли?» — именно поэтому в C# теперь нельзя проваливаться сквозь case — разработчики языка пытались максимально уменьшить число ошибок, допускаемых программистом («после case cтавь break»), но при этом сохранить гибкость С++ («юзай лучше goto, когда надо повторно использовать код внутри switch и выделять для этого отдельную функцию малоприемлемо»).
4) «Код должен быть скучной инструкцией, а не детективом» — если имеется ввиду, что goto «перебрасывает» через код, то вызов функции — это тоже «скачёк» к другому участку, в конце while, for тоже скачёк к началу цикла.

… У меня вёл преподаватель, который категорически запрещал goto, break (кроме как в switch), continue и несколько return-ов. Просто сказал бы «переписывай», если бы я заюзал «сами знаете что». Попытавшись с ним поспорить однажды нарвался на некоторую грубость и нежелание слушать. Не то, чтобы «запретный плод сладок», но в процессе своего самообразования я понимал, что это скорее предвзятость и «старая школа», а новая учит использовать весь доступный в языке инструментарий (иначе зачем его туда ввели разработчики языка — думаю первые, кто знает, что делают), но с умом.
упрощенная схема рассуждений такая:

в большинстве случаев goto не нужен,

остальные — спорные случаи: в них без goto сильно хуже не станет, дублирование кода всегда можно убрать

поэтому выбор: взять правило «без goto» и закрыть спорные случаи, либо каждый раз думать, можно ли здесь goto — самое главное, не при написании, а при прочтении чужого; как следствие, тратить время на споры и обсуждения, часто бесполезные — когда очень умный падаван защищает свое право на нестандартный код. Особенно когда под «спорные» случаи подтягиваются заведомые косяки.

Я выбираю взять жесткий гайдлайн, чтобы меньше думать и тем более меньше спорить :) Потому что человеческий фактор очень влияет на разработку, но это мало кто понимает, в спорной ситуации сосредотачиваясь на текущей конкретной проблеме, а не на том, что каждое решение влияет на весь наш путь:
«посеешь поступок — пожнешь привычку; посеешь привычку — пожнешь характер; посеешь характер — пожнешь судьбу»
В таких случаях — да, и это бывает минусом:

когда распинаешься перед программистом, объясняешь, почему и как, каких проблем это позволяет избежать, а потом он все равно делает так, как ты когда-то делал, но давно перестал, — потому что он и проблемы эти считает смехотворными, и что он на такие грабли не наступит.

И только постепенно, потратив кучу общего времени и нервов, понимаешь, что твое чутье — это чутье «заднего места», что нужно сначала сесть на кактус, чтобы потом его замечать заранее. Но всегда остается вопрос — где позволить ошибиться, а где постараться убедить сразу, так как ты за все отвечаешь, и как обойтись без авторитарности. Но потом и этому учишься )
(Опыт) ~ ((Количество кактусов, на которые сел) = (Количество граблей, на которые наступил)) (с)

Меня переучит только необходимое количество кактусов.
Дай бог не вторая революция GOTO в интернете…
Я бы сказал жутчайшее форматирование ТЕКСТА.

Основной посыл был понятен с самого начала, а потом еще 15 серых заголовков и 25 выделений болдом какбэ доказывают нам…

Одного — двух параграфов хватило бы, чесслово.
А ещё я всё ещё помню, что в Google C++ Coding Standards исключениями пользоваться не разрешают из-за того, что с самого начала писали без исключений, а теперь есть шанс нарваться на нетривиальные проблемы, когда код, не использующий исключения, начнёт вызывать код с исключениями. Вот так. Взяли и запретили ;-)
Sign up to leave a comment.

Articles