Comments 277
Для меня правило хорошего тона всегда брать в скобки и выносить на отдельную строку/в отдельную переменную то, что вызывает повышенное шевеление извилинами в во всём остальном обычном коде. Не вижу никакого смысла постоянно напрягать память для того, чтобы вспомнить каким именно образом компилятор или интерпретатор обрабатывает тот или иной уникальный случай. Проще написать больше тупого кода, чем меньше излишне умного. Сложно придумать аргументацию против такого подхода.
Хотя в последнее время читая код весьма продвинутых программистов вижу на удивление большое количество без необходимости сокращенных названий переменных, невыразительных названий функций/методов и полное отсутствие малейшего комментирования происходящего. Может это я такой тупой, но всё же не вижу никакого смысла в сокращении privateKey до pk и прочих подобных, ибо код превращается в кроссворд из кучи переменных каждая из которых содержит максимум 3 символа, а чаще вообще 1.
Я имел ввиду гораздо менее понятные сокращения) Против указанных в общем случае не имею ничего против.
А z там есть, но спрятан.
Вероятно это будет fi и la (φ и λ) или lat и lon.А почему не φ и λ?
phi и lambda?
Два вложенных фора c i/j очень часто на этапе написания содержат ошибку.
Прокладку, между стулом и клавиатурой (=
Значение этих переменных очевидно настолько, что замена мнемониками пользы не принесет. Есть такое правило: чем меньше скоуп жизни переменной, тем короче имена. Если в таком цикле возможно даже i с j перепутать, то тут уже стоит подумать о том, как его отрефакторить.
На самом деле, стоит задуматься о шрифтах в любимом редакторе (или IDE).
Я делаю имена итераторов и счётчиков цикла короткими, но мнемоническими. Например, итераторы/счётчики для addresses, users, rows, columns будут называться a, u, r, c, а не a, b, c, d (и не i, j, k, l). Если одной буквы не хватает для уникальной идентификации (например, columns и cells, или tests и testTargets) — двумя или даже полными словами.
Я делаю имена итераторов и счётчиков цикла короткими, но мнемоническими. Например, итераторы/счётчики для addresses, users, rows, columns будут называться a, u, r, c
Использование i,j,k в качестве индексаторов — это общепринятая практика, а однобуквенные штуки намекающие на тип будут уместнее в foreach-циклах, где это не просто индекс, а экземпляр, обладающий состоянием и поведением.
Меня же коробит что используют неочевидные языковые конструкции, наталкивающие на неправильные мысли
Любая наука обрастает своей профессиональной терминологией, что поделать?.. Или вы предлагаете «хреновинами» и «фиговинами» оперировать? (=
Если для вас это неочевидно, то это печальненько, ибо рассказывают это студентам на первом курсе.
Я бы написал так: это не просто индекс, а элемент структуры данных.
Сложными словами я как раз подчеркивал, что индекс != элементу коллекции, это всего лишь подярковый номер, без «состояния и поведения».
А если это шаблонный метод?
А, я понял. Вы вспомнили про ту глупую традицию называть параметры шаблонов T, T1, T2, …? Ну так это тоже нафиг.
Параметры шаблонов должны иметь осмысленные имена. Стилизания их названий должна быть такая же, как и стилизация того, что они обозначают (если параметр-тип и типы мы пишем с большой буквы, то с большой; если параметр-константа и константы мы пишем так-то, то так-то). MySuperCollection<Item> (или на худой конец MySuperCollection<ItemType> — хотя это спорно, мы ведь называем Integer и CustomerInfo, а не IntegerType и CustomerInfoType — ну да ладно), но не MySuperCollection<T>.
Другое дело — функции, делающие что-то тривиальное, когда кроме «v1», «v2» сложно выдумать что-то подходящее. «firstValue/secondValue»?
Дело в том, что целочисленные индексы используются всегда совместно с коллекцией: users[i]
. При этом вся необходимая для понимания семантики информация уже сосредоточена в имени коллекции.
Итератор же используется как самостоятельный объект, будучи синтаксически оторван от связанной с ним коллекции.
i,j,k — вполне применимы в простых случаях без вложенных циклов, хотя и не помню когда последний раз их использовал, т.к. легко заменяются на foreach.
А вот при работе с таблицами я бы предпочёл видеть rowIdx и colIdx — чуть больше писать, зато позволяет избегать детских, но труднонаходимых ошибок в коде.
Где-то есть золотая середина.
нихрена не понял откуда она и что берётТ.е. функция была сложна и непонятна?
логика работы (то, что функция должна была делать) перенесена в тело циклаТ.е. тело цикла и функции, в котором он находится, стало ещё более сложным и непонятным? А в чём профит Вашего действия?
Функция вызывалась один раз, называлась длинно с использованием непонятных терминов, передавала всякое туда-сюда по значению.
Заменилась на три строки в теле цикла.
Если бритва Оккама позволяет отсечь эту функцию, то вполне норм от нее избавиться, считаю.
Имеет смысл запаковывать логику в методы, когда это действительно имеет смысл (вот такая тавтология) — то есть, если там предполагаются изменения, либо она относится к какой-либо отличной зоне ответственности, нежели вызывающий метод, либо имеет место DRY, либо еще что.
Если там дествительно три строки, да и еще название не соответствует, возможно, там когда-то раньше было больше, а потом разработчик это убрал, да поленился остатки перенести назад или переименовать нормально. Избавление от старого хлама есть часть рефакторинга.
Ну а золотая середина (имхо) в том, чтобы придерживаться четких принципов и методологий ака SOLID, GRASP и прочих, которые как раз помогают четко детерминировать границы когда "так" делать, и когда "так" не делать.
Комментарии в чужом коде — большая моя боль. Бывает, пытаешься понять что-то в какой-то OpenSource библиотеке, открываешь какой-нибудь исходный файл и не видишь ни единого комментария, кроме лицензии, и хорошо, если это 500 строк, а не 5000. И так постоянно. Зачем они так делают?
Сам я оставляю много комментариев, возможно, слишком много. Стараюсь писать что делает какой-то кусок кода и с какого перепуга я написал его именно так, если это не очевидно. Во-первых, легче разобраться, а к тому же IDE комментарии подсвечивает и они очень наглядно отделяют блоки кода. В коде без комментариев даже глазу не за что зацепиться.
Зависит от того, как ещё переменные и методы обозваны. При хорошему подбору названий необходимость в комментариях резко уменьшается.
Проблема в том, что иногда не понятно, что писать в методе. Он часто говорит сам за себя, если вы находитесь в контескте логики приложения.
Я работал с программистом, который делал метод, к примеру, «FindMemo», и оставлял комментарий: «Файндит мемо». Несмотря на то, что добавить ему было нечего, понятней этот кусок кода не становился.
Так о том и речь — что добавление комментария не добавляет смысла. Если метод назван понятно — то и комментарий не нужен. Если метод назван непонятно — то и комментарий будет таким же непонятным :-)
Опять же, если взять мой пример с «FindMemo», рядом есть дефолтный метод Find. Для чего нужно было делать кастомный, и что такое memo в текущем контексте можно узнать только если детально разобраться в методе. Эту информацию и нужно было оставить в комментарии
Вот только вместо нормального комментария был оставлен комментарий "Файндит мемо". Почему? Потому что программист думал что это понятно.
Чтобы написать правильный комментарий, нужно было чтобы программист осознал что "Файндит мемо" — непонятно. Но в таком случае что помешало бы ему и метод тоже назвать по-другому?
Обычно надо не метод переименовывать, а подробней описать аргументы и результат (ну, как обычно описаны функции у ms/apple/..., и в doxygen/… удобно делать)
Категорически не согласен с таким — слишком длинное (и написанное на грамотном английском) название может быть неудобным в использовании или ещё что-то, но не непонятным.
Жаль, что существующие языки/IDE не предлагают вменяемых механизмов сокращения названий функций (типа, вообще она называется длинно и в достаточно удалённых модулях её будут называть полностью, но в нашем модуле, имеющем с этим набором функций дело часто, мы будем называть их так-то, так-то и так-то, а не полностью).
А длинные имена (едва ли там три слова дают в сумме 50 символов) говорят о том, что метод скорее всего нарушает SRP. (Было бы неплохо посмотреть на реальный пример)
А длинные имена (едва ли там три слова дают в сумме 50 символов) говорят о том, что метод скорее всего нарушает SRP.
Допустим, некая функция извлекает элемент определенным образом, так что просто ExtractItem недостаточно внятное название.
некая функция извлекает элемент определенным образом, так что просто ExtractItem недостаточно внятное название.
Нужен какой-то хороший пример. Что за айтем, откуда мы его берем? Если добавится еще 2-3 слова, то ничего страшного в этом нет. В названии нужно описывать цели, а не алгоритмы.
Тут комментарий и не нужен. Я имел в виду, что при вынесении в функцию теряется контекст, откуда и при каких условиях она вызывается. Примера под рукой нет, просто иногда такие случаи встречались. В основном, это были какие-то функции для частных случаев, которые вызываются в теле цикла или внутри сложного условия.
Есть такая штука: разбиение метода на два и более.
Если вам хочется назвать метод более чем 3-мя словами, то либо у вас словесный понос, либо ваш метод делает слишком много одновременно и нарушает хотя бы принцип единственной ответственности (single responsibility).
Не в этом дело.
Это редкие случаи, но вполне реальные. Я сейчас не буду искать по коду эти редкие случаи, вот синтетический пример: у вас есть метод, извлекающий из коллекции элемент по некоему идентификатору целочисленного типа. И еще один метод, извлекающий элемент по целочисленному идентификатору, но другому.
Соответственно, у ва сбудет метод ExtractItemByID (например), и, условно, ExtractItemByOriginalID
Если разбивать методы дроблением до совершенно малых, то теряется логика работы, как хорошо их ни назови. Более того, придется называть наоборот, более многословно, чтобы пояснить, что же именно вот этот абстрактный оторванный от жизни кусок делает; тогда как в норме это была бы часть другого, более крупного метода и не нуждалась бы в пояснениях ни через комментарии, ни через название в силу наличия контекста.
Если разбивать методы дроблением до совершенно малых, то теряется логика работы, как хорошо их ни назови.
Не теряется, если разбивать по SRP.
Соответственно, у ва сбудет метод ExtractItemByID (например), и, условно, ExtractItemByOriginalID
Что с ними не так?
Это довольно сложный вопрос. Если ваш программный продукт построен на фреймворках и там нет ни одной общей строки — тогда да.
Или вот, например, бывают такие методы.Возможно не очень понятно, что он делает сам по себе, но если вы находитесь в контексте работы приложения (а именно, это бот для работы с чатами), то в целом становится понятно.
Проблема в том, что если вы в целом не понимаете приложение или не находитесь в его контексте, то комментарии вам и не помогут. А если находитесь в контексте, то бывают и не нужны. Понятное дело, если пишется какой-то кастомный метод по какой-то причине, то лучше ее указать, но в целом довольно сложно писать комментарии к каждому методу и это выглядит как капитанство.
Через полгода или просто если его другой человек откроет, уже будет сложно понять и контекст и смысл метода.Агрумент про полгода, пожалуйста, уберите. Я его слышу уже больше 10 лет, но открываю свой код 10-летней давности… и эффекта не наступает. Да, конечно, мне приходится немного почитать свой собственный код, чтобы «вьехать» в то, что он делает — ну так и комментарии мне пришлось бы читать, какая разница?
Иногда люди не очень дружат с английским и использование становится странным и непонятным
Вы же понимаете, что код надо уже вчера, а английский минимум через неделю? А писать особо и некому, и чувак, по-случаю, вполне неплохо общается с ООП, алгоритмами и прочим, но вот английский дальше "a boy ate an apple" не зашел, но готов доблестно бороться с Google Translate.
И другим нет места в программировании?Им скоро не будет места почти нигде. Пока ещё не так, но к этому всё движется. Мир интегрируется — хорошо это или плохо. Причём в программировании это происходит чуть быстрее ввиду совершенно естественных причин. Не умеешь читать/писать/разговаривать по-английски — считай, не умеешь читать/писать/разговаривать вообще. Конечно, людям с объективными ограничениями (болезнь, возраст и т.п.) стоит сделать скидку, я не спорю. Конечно, теоретически ситуация может поменяться (деглобализация, другой мировой язык или ещё что-то). Но пока выглядит как-то так.
На самом деле никто не знает, какой именно набор умений наиболее важен (или оптимален при каком-то складе характера). Уж, по крайней мере, не уже-владеющему навыком об этом судить (а вот кого-то, кто бы говорил: «я не умею то-то и из-за этого пострадал там-то» — можно было было послушать). Допускаю, что существуют паттерны жизни, не завязанные на английский, просто я в них не разбираюсь.
открываешь какой-нибудь исходный файл и не видишь ни единого комментария, кроме лицензии, и хорошо, если это 500 строк, а не 5000. И так постоянно. Зачем они так делают?Не зачем, а «почему».
Мы с одним моим хорошим знакомым по этому поводу регулярно спорим. Для меня программа — это описание решения задачи, но она, в первую очередь, написана на C++ (Java, C#, PHP — нужное подчеркнуть). А комментарии — это налоги «сносок в тексте», поясняющих непонятные или странные моменты, которые никак не удаётся выразить на языке программирования.
Соответственно нормальное для меня количество комментариев — один-два на пару сотен строк кода и каждый раз когда возникает желание написать комментарий я, скорее, подумаю на тему как отрефакторить код, чтобы он был понятен и без этого.
Для моего же знакомого комментарии — это основное, текст программы — это, так, «что-то такое для компилятора», а описание программы должно быть на «человеческом» языке. И он начинает «кипятком писать», когда в ответ на вопрос «а что сюда, собственно, передавать» он получает ответ «ну из кода же это очевидно!».
Почему среди OpenSource преобладают люди первого вида, а «за деньги» — в основном работают люди второго вида я не знаю…
комментарии — это налоги «сносок в тексте», поясняющих непонятные или странные моменты, которые никак не удаётся выразить на языке программирования.Иногда бывает, что красивый, стройный, понятный алгоритм… тупит. Выполняется не приемлемое количество времени. И вот тогда начинается черная магия с рекурсией, вложенными циклами с next/prev, временными переменными, etc. Как вы считаете — надо оставить понятно и медленно, или странно и быстро — но с несколькими строчками комментариев, объясняющих «магию» (и предупреждение «ничего не трогай!» :) )
И да — это как раз такое место где в книге вы можете обнаружить врезку (или сноску) на две страницы, обьясняющую что здесь происходит (возможно с отсылками на другие работы и прочее).
Я не против длинных комментариев, более того — мой рекорд это описание строчек примерно в 50 к фукции из одной строчки (с отсылками на места в разных версиях C и C++ стандартов, обьясняющих почему этот код не просто «случайно здесь работает», а будет работать на всех реализациях, совместимых со стандартом).
Но сам принцип — «комментарий == сноска с пояснением в книге» для меня по прежнему является основным…
Надо оставить красивый-стройный-понятный и написать рядом оптимизированный.
С комментарием, что второй эквивалентен первому и набором тестов, которые должны проходить оба варианта.
Они просто приходят к 9 и уходят в 6.
Вы так говорите, будто это что-то плохое.
не фиксить их проактивно, ждать пожара… В ответ надо выдать эстимейт достаточный для хорошего рефакторинга
Хитрая тактика (= Хотя, если вы не зарылись в «пожарах», вокруг все не так уж плохо.
А о менталитете, в котором пребывание на рабочем месте важнее, чем выполненная работа и ее качество.
Мне кажется, что проблема здесь начинается с начальства, которое не волнует качество, а нужно видеть озабоченные лица за компьютерами с 9 до 18. В гос.конторах и других крупных бюрократических организациях такое практикуется.
Т.е. надо писать код так, чтоб комментарии вообще не требовались… Ибо часто бывает, что поменяв код или скопировав — комментарий остается без изменения и может обманывать программиста. Плюс такого подхода — названия переменных и функций должны быть гипер-продуманными и понятными. Ну и кода без комметариев на мониторе больше видно.
Видимо, в open source кадый мнит себя ЧакНорисом :)
Ибо часто бывает, что поменяв код или скопировав — комментарий остается без изменения и может обманывать программиста.Если комментарий противоречит коду, то есть проблема, да.
Иногда проблема в том, что код не соответствует замыслу программиста с самого начала.
А если комментария нет, то проблема не видна: код соответствует тому, что он делает, а не тому, что он должен делать.
Иногда проблема в том, что код не соответствует замыслу программиста с самого началаЕсть такая шутка: код делает то, что написал программист, а не то, что он хотел написать :)
Есть такая шутка: код делает то, что написал программист, а не то, что он хотел написать :)В десятку. Код всегда соотвествует программе. Он может не соответствовать замыслу, он может не соответствовать каким-то великим идеям, но он всегда верно и точно описывает то, что делает программа на самом деле.
А чтобы работать с программой мне, в общем-то, это и нужно. Какое мне, собственно, дело, до тех идей, которые роились в голове у человека, когда он это писал? Да никакого! Мне важно как оно здесь и сейчас работает!
Да никакого! Мне важно как оно здесь и сейчас работает!
А почему вы по умолчанию считаете, что оно работает? :-)
Другое дело, что оно может не делать того, что нам нужно — но почему вы считаете, что человек не способный написать работающий код сможет написать при этом толковый комментарий?
Потому что я видел такие комментарии.
Потому что он знает, что ему нужно, но не возможно не знал (или знал неправильно), как выразить это средствами языка и библиотек.
Возможно где-нибудь в аэрокосмической отрасли написание программы дважды (один раз в комментариях, второй — «перевод» на язык программирования) имеет смысл, но в большинстве случае IMNSHO это излишне.
Ну обманывает, прям проблема. Заметил, что не соответствует коду — возьми и поправь. При кардинальном изменении логики и комментарии удаляются. А при небольшом в них все равно есть полезная информация. Даже бывает, что становится понятна логика написавшего, и где ошибка. Конечно это если комментарий по делу. Бесполезные комментарии, дублирующие код, не нужны.
Ну обманывает, прям проблема.Да, таки проблема.
Заметил, что не соответствует коду — возьми и поправь.Этого невозможно заметить, если вы не читаете код, а читаете только комментарии. А если вы читаете код и понимаете его настолько, что можете исправить и комментарий — то зачем вам там комментарий вообще? Он только к лишней трате времени приведёт.
Бесполезные комментарии, дублирующие код, не нужны.Тем не менее я видел кучу стайл гайдов, которые требуют обязательно описывать все функции в поноценном doxygen-стиле. В результате имеем кучу комментариев тупо дублирующих код.
Этого невозможно заметить, если вы не читаете код, а читаете только комментарии.
А кто сказал, что надо читать только комментарии?
то зачем вам там комментарий вообще?
Затем, что он описывает то, чего в коде нет. "Здесь сделали так, потому что быстрее работает".
обязательно описывать все функции
Это документация, а не комментарии, дублирующие код. Внутренняя реализация может измениться, но если внешнее поведение не изменилось, то и документация не изменится. И наоборот, документация к функции может дополняться без изменения кода. Документация — это не просто комментарии, для нее иногда даже особый формат комментирования делают.
А кто сказал, что надо читать только комментарии?Никто не сказал. Но обычно люди, жалующихся на острый недостаток комментариев, как выясняется, код читать не хотят вообще. Или, по крайней мере, читают его когда что-то непонятно из комментария. Что, как мне кажется, извращает саму идею довольно сильно: комментарий должен прояснять код, а не код — являться разъяснением спорных моментов в комментарии!
Это документация, а не комментарии, дублирующие код.В 90% случаев функции делают что-то относительно несложное (что именно — описано в названии, параметры описывают что на входе и что на выходе) и это именно что дублирование даже не кода, а заголовка функции. В случае, когда функция делает какое-то нетривиальное действие комментарии, разумеется, уместны — но таких функций, обычно, немного.
Жалоба была именно на то, что нет комментариев «вообще», а не на то, что какое-то сложное место не описано.
Бывает, пытаешься понять
Комментарии в таком количестве кода это некоторые точки, в которых можно быть уверенным, что понял правильно, и от них уже отталкиваться.
Ориентироваться же по комментариям в коде — это всё равно что пытаться понять что есть в книге не по оглавлению, а по сноскам.
Т.к. многие функции возвращали указатели, постоянно приходилось угадывать, нужно ли потом память по этим указателям освобождать самостоятельно? Или нужно вызывать g_object_unref? Или можно вообще ничего не делать?
Ох и намаялся я, пока все утечки памяти выискивал.
Вот потому-то и надо умные указатели использовать...
const char *
arv_device_get_string_feature_value (ArvDevice *device, const char *feature)
Она возвращает указатель на строку. Эту строку нужно освобождать? Или не нужно? Комментариев к функции или внутри функции нет вообще. Строка прямо в ней не формируется, она берется из другой функции.
В итоге из-за отсутствия комментария (банально шапки к функции), приходится не просто читать код этой конкретной функции, а прослеживать весь путь, который эта несчастная строка проходит, прежде чем до моего вызова доберется.
Судя по git blame комментарии-шапки добавлялись для какого-то генератора биндингов, видимо, некомментированные функции этому генератору были не нужны.
Хех… типичный пример API функции у BLAS:
csymm (SIDE, UPLO, M, N, ALPHA, A, LDA, B, LDB, BETA, C, LDC)
Нужно больше боли ;-)
Я Вам более того скажу — молодые программисты на C++ написанием такого кода просто бравируют — "я могу понять как этого будет работать/вычисляться, а моему тимлиду надо пойти освежить свои знания или поломать голову чтобы разобраться".
Это да, но когда я открываю чужой код, а там месиво из сокращений, понять что происходить очень и очень тяжело. Я бы поставил на то, что автору кода тоже через 3 года читать свой код тоже будет не очень легко.
Ещё бесят сокращения вроде privKey
(в последнее время работал с кодом для цифровых подписей). Почему не написать privateKey
, всё равно ведь IDE дописывает слова сама? Короче, вижу много сомнительных решений и делаю выводы для себя чтобы писать более приятный для чтения код:)
Комментарии конечно вещь полезная, но тут всё зависит от ситуации.
В качестве примера он привел одну успешную книгу популярного автора, который свято верил в то, что чем короче код, тем быстрее он работает.
Кто-нить знает что это за книга? Стало интересно почитать.
можно научить компилятор анализировать такой код и выдавать предупреждение
На уровне компилятора любая двусмысленность должна порождать либо Warning, либо ошибку компиляции по причине undefined behaviour. Странно, что разработчики компилятора об этом не заботятся, а потом приходится юзать всякие PVS Studio и пр.
Интересно бы услышать мнение Andrey2008
Я думаю, это ближе к особенностям языка. Зачем то же такие выражения как ++i и i++ были нужны…
С ++i
и i++
легко разобраться если в той строке i
больше никак не используется. А вот если используется трижды и в пятерых строчках подряд, то мозг начинает закипать. Нужно уметь держать баланс.
UB — это не ошибка компилятора, а невыполнение программистом контракта.
Например, компилятор быстро выполняет сложение (не проверяет потенциальное переполнение), но программист должен гарантировать, что если у него знаковые числа и переполнение возможно — он его не допустит.
В таком случае компилятор делает сложение с помощью одной инструкции, а не 5, но налагает определенный контракт на разработчика. Если программист не выполнил соответствующий контракт — то поведение не определено (собственно, UB), компилятор имеет право сожрать ваши ботинки.
> компилятор имеет право сожрать ваши ботинки.
Скорее, компилятор имеет право сгенерить код, который сожрёт ваши ботинки.
Вот и неплохо было бы Warning получать, чтобы не держать в голове все детали "контракта".
Будет слишком много ложноположительных срабатываний и warning просто будет после этого отключен.
В том же rust пошли немого другим путём и в debug-сборке, например, overflow при сложении знаковых целых приведёт к панике (исключению), но debug-сборка там обычно работает в разы медленнее. В release-сборке поведение при этом, как минимум, implementation specific, хотя, скорее всего, будет близко к UB из Си. А кому нужна именно сумма с переполнением напишет overflowing_add
явно.
https://doc.rust-lang.org/std/primitive.i32.html, см. checked_add
, saturating_add
, wrapping_add
и overflowing_add
. Выбираете нужный явно и никто потом не гадает что имелось ввиду и есть ли какие-нибудь особые куски контракта не отраженные в коде.
Оно формально intrinsics?
А мучения, видимо, чтоб жизнь мёдом не казалась. И в стандарт доедут году к двадцатому..
Оно формально intrinsics?Угу. Код достаточно оптимальный порождается, семантика тоже определена.
И в стандарт доедут году к двадцатому..Экий вы оптимист, батенька…
В release-сборке поведение при этом, как минимум, implementation specific, хотя, скорее всего, будет близко к UB из Си
В релизе поведение вполне чётко определено (насколько это может быть для языка у которого нет стандарта).
Мне кажется, компилятор или должен генерировать максимально близкий машинный код, передавая ответственность за поведение процессору (и выдавая предупреждение об этом), или не должен его вообще компилировать. А не трогать чужие ботинки.
Вы готовы пожертвовать 80-90% производительности для десктопа? Получить десятикратное увеличение стоимости услуг и программ? Если да, то есть простые компиляторы с простой кодогенерацией, но в конкурентной гонке general purpose софта побеждают отнюдь не надёжные и медленные программы, которые имеют 5-10% нужного функционала. Пока нет специальных требований на надёжность при допустимой низкой производительности (как в, например, hard realtime), никто этого делать не будет.
При этом есть большая ниша прикладного софта, где нет проблем с производительностью. И пишут её на java/c#/python, где проблемы UB не стоит, т. к. managed окружение с примерно одной виртуальной машиной (на каждую из платформ) за счёт TCK и подобных решений. В случае питона это не совсем так, но близко.
Откуда такие цифры? Хотите сказать, что 80-90% производительности для десктопа достигается за счет кода с UB?) Я предполагаю, что большинство кода написано без UB, следовательно, компилятор там ничего убирать не будет.
Я предполагаю, что большинство кода написано без UB
Без, но с учётом же. Таким образом у компилятора есть возможность проводить некоторые свои оптимизации.
Я предполагаю, что большинство кода написано без UB, следовательно, компилятор там ничего убирать не будет.Вы это серьёзно?
Пример кода, который потенциально может вызвать UB:
a = b + c;
. Или ещё так: free(p);
. Или, на худой конец: i++
.Много вы программ видели, которые ничего этого не содержат? Или где такого кода очень мало? Я видел — это обычно 100500 обёрток, которые и без всякого UB тормозят так, что им никакой компилятор не поможет…
Тем не менее, компилятор генерирует для этого выражения что-то вродеa = b + c
add eax, ebx
или аналогичного ему по поведению. А не запускает форматирование жесткого диска. Это я и имел в виду под «генерировать максимально близкий машинный код».Тем не менее, компилятор генерирует для этого выражения что-то вроде add eax, ebx или аналогичного ему по поведению.Простейший компилятор — да, может быть. Да и отптимизирующий тоже, если это выражение присутствует в отдельной функциии и ничего другого там нет — но кому такая функция нужна?
А вот если «чего другого» там есть, то оптимизирующий можен много чего со всем этим сделать. Может засунуть константу прямо в оператор обращения к памяти, например. Или перенести её куда-то. Да много чего можно сделать если знать, что переполнения не будет. А его не будет, так как программист обещал!
И как ни странно, это тоже «максимально близкий машинный код», потому что дает тот результат, который требуется — в a будет сумма b и c.Не будет в
a
суммы b
и c
. Потому что оно туды не влезет. В результате у вас индекс, который, вообще говоря, планировался быть short
'ом после оптимизаций окажется равным 70000 — и вы будете материть компилятор на чём свет стоит.Даже если a появляется только во внутренних регистрах процессора при вычислении сложной адресации.В том-то и дело, что нет — там появляется
a
только в том случае если программист позаботился об этом и написал программу так, что она не вызывает UB.А дальше, если за этим не следить, получается «снежный ком» — тут у нас оказалось 70'000 в short'е, там — мы полезли не в тот обьект, вынули не то, засунули не туда… и вот уже ваша программа самоуничтожается. До форматирования винчестера дело [пока?] не дошло — но всё ещё впереди!
Если после оптимизаций получилась сумма 70000 при входе 2 и 2, значит это неправильные оптимизации. Если же программист сознательно складывает 60000 и 10000 в short, то он не будет материть компилятор, когда не получит 70000. Потому что это понятное и логичное поведение. Тем более, что компилятор его предупреждал. Поэтому непонятно, откуда у вас получился снежный ком.
В том-то и дело, что нет — там появляется a только в том случае если программист позаботился об этом и написал программу так, что она не вызывает UB.
Вот я и говорю, так быть не должно. Написано, значит должно появляться, или не компилироваться. А не модифицироваться молча с другим поведением в результате модификации.
А где грань? Почему в случае a = b + c
предполагается доверять программисту что тот ничего не забыл — а в случае, условно, a = b->c;
надо программиста предупреждать?
Если же программист сознательно складывает 60000 и 10000 в short, то он не будет материть компилятор, когда не получит 70000.В том-то и дело, что может получить в результате оптимизаций.
Тем более, что компилятор его предупреждал.Он его на каждую операцию сложения предупреждал? И программист этого не отключил? Это какой-то неправильный программист и он явно занимается неправильным делом. Ему мандалы из песка делать нужно.
Поэтому непонятно, откуда у вас получился снежный ком.Снежный ком получается когда у вас одна «невозможность» цепляется за другую «невозможность». Ниже я пример разобрал.
Написано, значит должно появляться, или не компилироваться.Есть маленькая проблеммка: узнать — вызывает программа UB или нет, в общем случае, невозможно. Проблема остановки, мать её. Потому компилятор исходит из того, что программист — сам себе не враг, UB не допускает и из этого исходит.
О самых вопиющих случаях компиляторы сообщают.
В том-то и дело, что может получить в результате оптимизаций.
Мы же про 16-битный short? Откуда там будет 70000, если для него надо 17 бит?
Он его на каждую операцию сложения предупреждал?
Я не особо специалист в C++, наверно чего-то не понимаю. То есть, сейчас в программах любая операция сложения это UB, с которой компилятор может сделать все что угодно?
вызывает программа UB или нет, в общем случае, невозможно
Ну компилятор же как-то принимает решение, выбросить этот код так как UB или нет.
Мы же про 16-битный short? Откуда там будет 70000, если для него надо 17 бит?На многих процессорах нужно предпринимать специальные усилия, чтобы «обрезать» число (x86 — редкое исключение, а не правило) и компилятор, зная о том, что переполнений не бывает вполне может производить вычисления с большей точностью. В x86 так тоже может быть — но в довольно специфических условиях.
То есть, сейчас в программах любая операция сложения это UB, с которой компилятор может сделать все что угодно?Любая операция сложения потенциально может приводить к UB, если результат «не влезет» в соответствующий тип. Компилятор, в общем, не так часто может понять — будет результат «влазить» или нет. Так что «с консервативным подходом» ему придётся выдавать предупреждения на каждую операцию сложения, про которую он ничего не сможет доказать. То есть про большинство из них.
в стандарте языка есть вещи, которые компилятор обязан отслеживать. И они это делают. Соответственно, все оптимизации производятся с учетом этих требований. А неопределенное поведение потому и неопределенное, потому что даже разработчик компилятора не скажет, во что оно выльется.
Так разработчик компилятора и не должен ничего говорить. Неопределенное значит неопределенное, пусть процессор и ОС разбираются. Не надо его выбрасывать или заменять на другое, как в примере с циклом. Даже там в комментах шутят про "rm -Rf /". Просто по-моему это не то, что должен позволять стандарт языка.
Причем здесь "смиритесь"? Это обсуждение причин и возможностей, а не pull request в стандарт. Поговорить на эту тему теперь тоже нельзя?
Я высказываю аргументы в защиту своей точки зрения. Если я где-то рассуждаю неправильно, укажите где и приведите свои аргументы. Где именно здесь невежество, да еще и воинствующее?
mov eax, ebx
" (вот прямо подряд, без зазоров), Их вроде бы можно заменить на один mov
. Я не знаю — как это «заметить». Хотя, может быть, и тут можно, просто у меня недостаточно богатое воображение.Но много вы на таких оптимизациях получите? А просто заменить «лишний»
mov
, как было нашим горе-воякой предложено — нельзя.P.S. Языки без UB — это языки не дающие использовать несколько потоков и на дающие возможность напрямую манипулировать памятью. Иначе — никак. Даже в Go и Java — полно UB, потому что манипулировать миром, на который кто-то может смотреть «сбоку» и что-то там изменить так, чтобы этого не стало заметно — практически невозможно.
Если у вас в программе написано два раза "mov eax, ebx" (вот прямо подряд, без зазоров), Их вроде бы можно заменить на один mov.
При чуть более сложном варианте вида mov eax, [esi]
два раза уже, по сути, заменить два mov
а на один нельзя даже в однопоточном коде при запрещенных начисто прерываниях. Вдруг по адресу из esi
лежит mmio-регион.
А просто заменить «лишний» mov, как было нашим горе-воякой предложено — нельзя.
Это был просто пример, вырванный из контекста. Выбрасывание этой инструкции аналогично выбрасыванию любого другого кода. Чем это принципиально отличается от оптимизации в вашем примере?
Строго говоря ему ещё не удалось привести пример оптимизации, которая не приводила бы к неработоспособности чьй-нибудь программы.
Покажите пожалуйста, где я утверждал, что оптимизация должна гарантировать работу стороннего кода, считающего такты или что-то подобное? Я привел пример с разворачиванием цикла, это вполне нормальная оптимизация. Чем это вас не устраивает?
А в вашем примере оптимизация на основе UB привела к неправильному выполнению программы.
Языки без UB — это языки не дающие использовать несколько потоков и на дающие возможность напрямую манипулировать памятью.
Я не пытаюсь предложить язык, результат компиляции которого будет полностью без UB, и нигде такого не утверждал. Я считаю, что компилятор наоборот не должен полагаться на UB и должен оставлять его как есть.
Да. "Можно 8 раз скопировать действия без цикла и выбросить сравнение i".
Как раз здесь вполне можно оптимизировать вплоть до того, что ничего не повторять
В том примере ничего не повторять не получится, так как заполняются значения по указателю. А в целом да, если в цикле одно действие x = 2;
, то цикл можно убрать, так как результат не изменится. Здесь нет никакого противоречия моим словам. "значит надо делать его 8 раз" было сказано в контексте того примера, так как там поведение цикла распространяется за пределы цикла и функции.
Чем это принципиально отличается от оптимизации в вашем примере?Ничем — и в этом-то всё и дело.
Покажите пожалуйста, где я утверждал, что оптимизация должна гарантировать работу стороннего кода, считающего такты или что-то подобное?Дык эта:
Я считаю, что компилятор наоборот не должен полагаться на UB и должен оставлять его как есть.Вот прямо-таки туточки.
Вы либо трусы наденьте, либо крестик снимите. Либо у вас оптимизатор имеет право поломать программу, которая вызывает UB («считает такты или что-то подобное»), либо нет.
Потому что пока ваши хотелки выглядят так: копилятор имеет право делать для оптимизации что угодно — но не должен ломать моих программ… чужие — можно.
Так не бывает, уж извините. Нужен полный и точный перечень того, чего «хорошая», «правильная» программа делать не должна (в частности, по вашему — не должна считать такты). И вот этот список — и есть список UB, на которые полагается ваш компилятор.
Компилятор, который оставляет все UB как есть — не может оптимизировать, фактически, ничего и никак…
Ничем — и в этом-то всё и дело.
Почему вы тогда сказали, что "просто заменить нельзя", если компиляторы проводят такие оптимизации? Раз ничем не отличается, значит можно.
Вот прямо-таки туточки.
Не вижу связи. Не могли бы вы прямо по пунктам написать логические выводы, приводящие к противоречию? Мне правда интересно, возможно я что-то не так понимаю.
Оптимизации бывают не только из-за UB. И не каждую ситуацию из тех, которые в стандарте C++ называются UB, нельзя оптимизировать. Я ниже привел примеры под спойлером, что я имею в виду под "не должен полагаться на UB".
чужие — можно
в частности, по вашему — не должна считать такты
Нет, это не по-моему, это вы сами додумали. Я наоборот говорю, что не должно быть требований, что программа что-то должна. Считает — ее дело, пусть программист учитывает, что может быть оптимизация.
Либо у вас оптимизатор имеет право поломать программу, которая вызывает UB, либо нет.
Оптимизатор должен обеспечивать то же поведение. Если до оптимизации было 2 граничных случая, то и после нее должно быть столько же. А не a = 42;
не может оптимизировать, фактически, ничего и никак…
Я же привел примеры. Развернуть цикл можно? Можно.
Оптимизации бывают не только из-за UB.Оптимизатор полагается на то, что некоторые действия являются UB. И, соотвественно, в программе не встречаются.
Почему вы тогда сказали, что «просто заменить нельзя», если компиляторы проводят такие оптимизации?Соблюдая ваши «правила игры» (компилятор наоборот не должен полагаться на UB и должен оставлять его как есть) — нельзя. По правилам игры, в которую «играют» разработчики компиляторов — конечно можно!
Я наоборот говорю, что не должно быть требований, что программа что-то должна. Считает — ее дело, пусть программист учитывает, что может быть оптимизация.Круто. У вас буквально две соседние фразы противоречат друг другу! Я впервые вижу такое проявления двоемыслия в споре.
Но я вам маленький секрет расскажу: двоемысление — это хорошо для споров, плохо — для написания работающих программ.
Оптимизатор должен обеспечивать то же поведение. Если до оптимизации было 2 граничных случая, то и после нее должно быть столько же. А не a = 42;
Пример вашей «хорошей» оптимизации это требование нарушает. До оптимизации выход из цикла был возможен, после — нет.Я же привел примеры. Развернуть цикл можно? Можно.В общем случае — нельзя.
Рассмотрим практический пример:
uint64_t Read64A(const uint8_t* src) {
uint64_t result = src[0];
for (int i=1;i<8;i++)
result |= (uint64_t)src[i] << (i * 8);
return result;
}
Развёрнутый (и затем свёрнутый) цикл:
Read64A(unsigned char const*):
mov rax, qword ptr [rdi]
ret
Казалось бы — великолепная оптимизация, слава компилятору!
Одна беда — если теперь вы в эту память будете из другого потока писать либо 0x0000000000000000, либо 0xffffffffffffffff, то прочитать оттуда 0x00000000ffffffff вы не сможете ни за что и никогда. А оригинальный код это сделать мог. И это вполне могло помогать кому-то программу.
P.S. Только не надо про то, что «стало только лучше» и «скорее всего программист этого и хотел». Эти критерии — неформализуемы и их, в общем, невозможно использовать для того, чтобы решать — является ли что-то хорошей оптимизацией или нет.
Оптимизации бывают не только из-за UB
Не из-за а благодаря.
Если до оптимизации было 2 граничных случая, то и после нее должно быть столько же.
А откуда известно, какие «2 граничных случая» были до оптимизации? Учтите, что C — кроссплатформенный язык, и сколько там «граничных случаев» будет на той или иной платформе — неизвестно.
Развернуть цикл можно? Можно.
Какая разница, сегодня или месяц назад, главное собаку-то я покормил!
P.S. А цикл развернуть без предположений о возможности UB тоже нельзя, и вам уже несколько раз показали почему.
P.S. А цикл развернуть без предположений о возможности UB тоже нельзя, и вам уже несколько раз показали почему.О невозможности UB. Невозможности, которую должен обеспечить программист.
Любая оптимизация опирается на тот факт, что программу у нас «хорошая», чего-то «плохого» не делает. Вот если это «плохое» в принципе «железо» позволяет сделать — это и есть UB.
Понятно, что тут всегда встаёт вопрос компромиса: что именно мы можем заставить разработчика не делать, а чего не можем. То есть вопрос отнесения чего-то к UB — это вопрос обсуждаемый. Что-то, что стандарт называет UB, конкретный компилятор может и не называть UB, а, наоборот, допускать что в программе такое происходит. А может иметь свои UB (обычно считается, что это «дефект, который когда-нибудь пофиксят», но это когда-нибудь может растянуться на долгие годы). Но если уже что-то отнесено к UB — то, разумеется его в программе быть не должно и компилятор вправе на это полагаться.
Выдача же при этом полезных предупреждений — отдельная и весьма сложная задача.
А откуда известно, какие «2 граничных случая» были до оптимизации? Учтите, что C — кроссплатформенный язык, и сколько там «граничных случаев» будет на той или иной платформе — неизвестно.
Из исходного кода. Если в исходном коде присваивание было только при условии, то после оптимизации не должно присваиваться всегда. Тем более что в правильно написанном коде переменная должна быть инициализирована, и такой оптимизации не будет. PVS-Studio ведь как-то находит такие ошибки, несмотря на то, что C — кроссплатформенный язык.
А цикл развернуть без предположений о возможности UB тоже нельзя, и вам уже несколько раз показали почему.
Вы упорно не хотите понять, что я пытаюсь объяснить. Случаи, которые можно назвать UB, бывают разные. Если коротко, то что делает PVS-Studio, должен делать компилятор.
Тем более что в правильно написанном коде переменная должна быть инициализирована, и такой оптимизации не будет.Но если человек её не проинициализировал, то это значит что ему всё равно что оттуда вернётся — так почему бы и не 42? Кода меньше, а в правильно написанной программе — это «стрелять» не должно.
PVS-Studio ведь как-то находит такие ошибки, несмотря на то, что C — кроссплатформенный язык.PVS-Studio — это отдельный продукт, специально «заточенный» под то, чтобы отлавливать ошибки в программах. А компилятор — это компилятор. Компилятор решает одну задачу: сделать так, чтобы программа не вызывающая при своём исполнении UB работала быстро и требовала мало памяти. Всё остальное — не к компилятору. Не надо превращать компилятор в Die Eierlegende Wollmilchsau, пожалуйста, это приведёт только к тому, что все задачи будут решаться одинаково плохо.
Кстати, вы можете привести пример таких оптимизаций? Нашел в гугле пару ссылок, попробую объяснить, что я имею в виду.
Signed overflow
int foo(int x) {
return x+1 > x; // either true or UB due to signed overflow
}
may be compiled as (demo)
foo(int):
movl $1, %eax
ret
Это явно ошибка в логике — условие всегда истинно или происходит переполнение.
Access out of bounds
int table[4] = {};
bool exists_in_table(int v)
{
// return true in one of the first 4 iterations or UB due to out-of-bounds access
for (int i = 0; i <= 4; i++) {
if (table[i] == v) return true;
}
return false;
}
May be compiled as (demo)
exists_in_table(int):
movl $1, %eax
ret
Здесь происходит обращение за границу массива. Во первых, это ошибка в логике. Во-вторых, есть 3 возможных варианта — значение равно или не равно, или произойдет аппаратная ошибка чтения. Оптимизация предполагает, что там всегда будет равно, из-за чего до 2-го return не дойдет, то есть выбрасывает из рассмотрения остальные варианты. Ошибка скрыта, логическое поведение кода изменилось.
std::size_t f(int x)
{
std::size_t a;
if(x) // either x nonzero or UB
a = 42;
return a;
}
May be compiled as (demo)
f(int):
mov eax, 42
ret
В коде присвоение 42 только при определенном условии, а после оптимизации во всех случаях. И опять же, неинициализированная переменная это ошибка в логике.
https://www.cl.cam.ac.uk/teaching/1415/CandC++/lecture10.pdf
By knowing that values “cannot” overflow, the compiler can enable useful optimisations:
for (i = 0; i <= N; ++i) { ... }
If signed arithmetic is undefined, then the compiler can assume the loop runs exactly N+1 times.
Вот и пусть 'assume', потому что это то, что написано. Если там будет
i <= INT_MAX
, это будет известно на этапе компиляции, и можно показать предупреждение.Это между прочим те вещи, которые ищет PVS-Studio. Такая оптимизация маскирует ошибки, хотя задача компилятора эти ошибки находить. Программа работает быстро, но не так как надо.
У меня сложилось впечатление, что UB называют очень логически разные ситуации, часть из которых подходит для оптимизации, часть нет. Если вы считаете, что я не прав, значит так тому и быть. Я просто высказал мнение и постарался объяснить, почему я так думаю.
Это я понял. Я имел в виду, что они все-таки различаются, и работать со всеми одинаково нельзя, потому что это приводит к проблемам.
работать со всеми одинаково нельзя
Что значит «одинаково работать»?
Менять в любом из этих случаев код на любой другой, нужный для оптимизации, предполагая, что поведение будет такое же.
Менять в любом из этих случаев код на любой другой, нужный для оптимизации, предполагая, что поведение будет такое же.Извините, но кто будет решать — когда код менять можно, а когда — нет? И как?
Решать будет компилятор. Только надо разделить понятия "UB из-за неинициализированной переменной" и "UB так как возможно кто-то 8 байт пишет из другого потока". В первом случае это написано в коде, во втором нет.
Только надо разделить понятия «UB из-за неинициализированной переменной» и «UB так как возможно кто-то 8 байт пишет из другого потока».Они уже разделены. Я боюсь вы смешиваете два понятия: поведение, определяемое реализацией (что-то, что разные компиляторы могут делать по разному) и неопределённое поведение (то, чего в программе случаться не должно и то, чего программист не должен делать никогда).
В первом случае — компилятор обязан обеспечить некоторое разумное поведение (скажем если вы засунете 2147483648 в 32-битный
int
, то получите либо -2147483648, если у вас используется дополнительный код, либо -0, если у вас используется прямой код, но ничего «странного» при этом произойти не может), во втором — компилятор может делать всё, что угодно. Совсем что угодно.Зачем вам потребовалось ещё как-то этот волос расщеплять и кому от этого станет легче — мне неведомо.
В первом случае это написано в коде, во втором нет.Что значит «написано» и «не написано»? 8 байт кто пишет? И как? Пушкин? С того света, что ли? Конечно же в программе есть где-то код, где и эти 8 файт атомарно пишутся…
Такая оптимизация маскирует ошибки, хотя задача компилятора эти ошибки находить.Задача компилятора — компилировать. Для нахождения ошибок — есть другие инструменты. То, что компилятор, по совместительству, является ещё и статическим анализитором и линтером — это хорошо, но это не является его основной работой!
Так имеет право компилятор сделать такую оптимизацию или нет?int foo(int x) { return x+1 > x; // either true or UB due to signed overflow } may be compiled as (demo) foo(int): movl $1, %eax ret
Это явно ошибка в логике — условие всегда истинно или происходит переполнение.
У меня сложилось впечатление, что UB называют очень логически разные ситуации, часть из которых подходит для оптимизации, часть нет.Вы, в некотором роде, правы. Часть вещей, которые названы UB в стандарте можно бы и доопределелить. Скажем то же переполнение можно рассматривать в дополнительном коде — и, сюрприз, сюрприз, вы можете это заказать.
Все же UB, которые не «доопределены» подобным образом компилятор имеет право «использовать» — в смысле «трактовать в свою пользу».
Я просто высказал мнение и постарался объяснить, почему я так думаю.Вы бы вначале сформулировали своё мнение, а потом его высказывали бы, а? А то сейчас у вас получается что компилятор должен и предполагать, что программа «глупостей не делает» (где я утверждал, что оптимизация должна гарантировать работу стороннего кода, считающего такты или что-то подобное?), и предполагать, что они «глупости там таки есть» ( считаю, что компилятор наоборот не должен полагаться на UB и должен оставлять его как есть). Одновременно.
Давайте возьмем гугл и посмотрим на производительность tcc от Bellard'а:
https://groups.google.com/forum/#!topic/comp.lang.c/9l55qxm-S68, http://lists.nongnu.org/archive/html/tinycc-devel/2013-02/msg00039.html. Потери производительности от 2х раз до порядка (относительно gcc).
Замечу, что он хоть и относительно простой, но оптимизации там есть. И UB из языка C там никуда не исчезает, более топорная кодогенерация и меньше шансов на странные побочки от UB. Но при этом компилятор всё равно полагается на то, что программист не допускает нарушения соответствующих контрактов.
Без этого банальный инкремент int'а будет занимать не одну инструкцию, а 3-4 с условным переходом, что будет забивать мозг предиктору переходов (который аппаратный и "память" у него короткая), дёргать дополнительно ALU для сравнения или читать дополнительно флаги. Всё это спокойно может давать overhead в десятки тактов.
Или он выполняется одну инструкцию, но может, например, вызывать exception (аппаратный) при переполнении. Или дать неожиданный результат/повредить память где-то ещё, или что угодно.
Но при выполнении контракта (недопущении UB программистом) вы получите работу инкремента за одну инструкцию.
Без этого банальный инкремент int'а будет занимать не одну инструкцию, а 3-4 с условным переходом
Я говорю про абсолютно обратную ситуацию. Написано сложить две переменных в памяти и поместить в третью — генерируем инструкции для сложения и присваивания, выдаем предупреждение о возможном переполнении. Или вообще не компилируем. Но не додумываем за программиста и не выкидываем просто так.
более топорная кодогенерация
Это не то, что я имел в виду.
Написано сложить две переменных в памяти и поместить в третью — генерируем инструкции для сложения и присваивания, выдаем предупреждение о возможном переполнении.
После какого количества warning'ов вы их отключите? После первых нескольких тысяч? Или раньше?
Есть другой подход, с контрактами и доказательствами. Но это будет очень сложно и дорого в разработке. И, опять же, программист должен будет доказывать, что переполнения не случится.
Написано сложить две переменных в памяти и поместить в третью — генерируем инструкции для сложения и присваивания, выдаем предупреждение о возможном переполнении. Или вообще не компилируем.Это примерно то, что делает tcc. Потери производительности, как вам уже сказали — от 2х раз до 10. Хотя он не вполне неоптимизирующий.
Но не додумываем за программиста и не выкидываем просто так.Для этого нужно чётко описать что такое «не додумываем до программиста». И как это может соотноситься с вот этим:
А что вы имели в виду?более топорная кодогенерацияЭто не то, что я имел в виду.
В примере, который я уже приводил компилятор всего-навсего заметил, что у нас кроме счётчика цикла есть ещё и другая переменная, к которой прибавляется в цикле постоянно одно и то же число. И, стало быть, можно выкинуть все манипуляции со счётчиком цикла (а они, как бы, время занимают, да) — и использовать для выхода из цикла эту переменную. Законно так сделать? В отсутствие переполнений — да, разумеется. Простая линейная замена. Весьма и весьма полезная в разных матричных алгоритмах.
А дальше — другой код, про UB ничего не знающий проверил когда в «усовершенствованной» программе происходит выход из цикла. И выяснилось что для этого число должно стать больше максимального числа, который может поместиться в
int
. То есть у нас там — проверка, которая всегда ложна. То есть выход из цикла не случится никогда. Законная такая замена? Разумеется — опять-таки в случае отсуствия UB.А в результате — цикл вместо заполнения таблички уничтожает всю вашу программу на корню.
В том-то и дело, что вы либо «додумываете за программиста и выкидываете просто так» куски кода — тибо у вас «более топорная кодогенерация». А как иначе? Как, по вашему, компилятор должен что-то оптимизировать, если он ничего никуда не может подвинуть и ничего ни откуда не может удалить?
у нас кроме счётчика цикла есть ещё и другая переменная, к которой прибавляется в цикле постоянно одно и то же число. И, стало быть, можно выкинуть все манипуляции со счётчиком цикла
Неправильно. Во-первых, переполнения существуют, и компилятор должен об этом знать. Во-вторых, написано, повторить цикл 8 раз, значит надо делать его 8 раз. Написано, выйти из цикла, когда переменная i станет 7, значит надо сравнивать i и 7. Можно 8 раз скопировать действия без цикла и выбросить сравнение i. Но не заменять одну проверку другой. Это именно то, о чем я говорю — компилятор додумал за программиста. А дальше да, снежный ком.
Как, по вашему, компилятор должен что-то оптимизировать
Пример:
mov eax, ebx
mov ebx, eax
Заменяется одной инструкцией:
mov eax, ebx
Результат со всеми изменениями в состоянии регистров тот же самый.
Оптимизация? Оптимизация.
К тому же даже такая оптимизация имеет допущения, что никто не должен читать регистры между выполнением инструкций, что вы не считаете такты процессора.Ну считать такты в современных процессорах никто не будет, а менять регистры — таки да. Я с таким лично сталкивался в JIT-компиляторе. Но это редкость. Но если учесть ещё, что C, в общем-то, обычно не все переменные в регистрах (более того, в C++20 их вообще нельзя будет на регистры класть), то окажется, что вам вообще ничего трогать нигде нельзя будет — ибо sigaction/kill вдруг работать перестанут (ну или многопоточные программы вспомните, если у вас нет в OS работающих sigaction/kill).
Это применимо к любой оптимизации.
В том-то и дело, что любая оптимизация опирается на то, что код определённых вещей не делает и, соответственно, «не заметит» разницы между оптимизированной и не оптимизированной версией.
Правильный код на C/C++ не считает такты процессора, не меняет регистров из обработчика сигналов, не обращается к переменным, которые были освобождены c помощью
free
и к переменным вообще из разных потоков без синхронизации (за исключением строго описанных случаев — см. volatile
)… в общем код — не вызывает UB никогда и нигде.Вот тогда его можно как-то оптимизировать. Иначе — никак.
Я же написал, как можно сделать оптимизацию в приведенном вами примере без изменения ожидаемого поведения. "Ожидаемое" — это то, что написано в исходном коде. Значит иначе тоже можно.
Я же написал, как можно сделать оптимизацию в приведенном вами примере без изменения ожидаемого поведения.Вы написали чушь собачью. Ожидаемого кем? Ожидаемого когда?
«Ожидаемое» — это то, что написано в исходном коде.Нельзя. Для этого нужно «знать» что происходит во всей программе.
Рассмотрим ваш пример и немного его расширим.
Неоптимизированный (работающий) код:
mov 1, eax
back:
mov eax, ebx
mov ebx, eax
test eax, eax
jnz back
Оптимизированный (неработающий) код:
mov 1, eax
back:
mov eax, ebx
test eax, eax
jnz back
Как, почему, зачем, за что? А очень просто: я беру sigaction/pthread_kill и по сигналу из другого потока обнуляю
ebx
(если вы любитель Windows, то для вас есть GetThreadContext/SetThreadContext).И всё. Ваша «безопасная и надёжная» оптимизация превратила работающую программу — в неработающую. Неоптимизированный код работу завершает, опимизированный — нет.
И? Что вы после этого будете говорить? Что в программе «такого ужаса» быть не должно? Или что ваши оптимизации плохие?
Если первое — то вы только что сами ввели понятие UB. Если второе — то всё ещё остаётся вопрос какие оптимизации являются хорошими.
Ожидаемого кем? Ожидаемого когда?
Ожидаемого от программы кем-то, кому нужен ее результат. Ожидаемого в результате компиляции кода. Ожидаемого во время его работы. Что за философские вопросы?
Как, почему, зачем, за что? А очень просто: я беру sigaction/pthread_kill и по сигналу из другого потока обнуляю ebx
И? Что вы после этого будете говорить?
А причем здесь другой поток? Компилятор про него ничего не знает, у него есть только исходный код. Я уже несколько раз это говорил. Насколько я знаю, чтобы ему это сообщить есть ключевые слова типа volatile.
Ваша «безопасная и надёжная» оптимизация превратила работающую программу — в неработающую
И снова я напомню, что это был не рецепт на все случаи жизни, а абстрактный пример, оторванный от контекста конкретной программы с потоками или без и приведенный в контексте разговора про изменения машинного кода. И там и там есть изменения, но одни ломают однопоточную программу, а другие нет.
Вы написали чушь собачью.
Хороший аргумент. Пожалуй, на этом стоит закончить.
Ожидаемого от программы кем-то, кому нужен ее результат. Ожидаемого в результате компиляции кода. Ожидаемого во время его работы.То есть в компилятор должен быть встроен ещё и модуль телепатии, который будет читать в голове у автора его ожидания? Я боюсь вы переоцениваете способности создателей компилятора, однако.
Что за философские вопросы?Это не «философские вопросы». Это набор «правил игры». Если конструкция имеет некоторое определённое поведение, то, стало быть, есть некоторые ожидания от того, что будет делать программа в которой такая конструкция встретилась. Если же поведение не определено — то никаких ожиданий нет. Любое поведение — будет ожидаемым. Без каких-либо ограничений.
В этом сама суть неопределённого поведения. Оно потому и не «неопределённое», что никаких ожиданый у вас по поводу такой программы быть не может.
Компилятор про него ничего не знает,Как это не знает?
у него есть только исходный код.И в этом коде — есть функция, которая, из другого потока, меняет
ebx
. Так что знает он всё, конечно — вопрос только в том, что использовать это знание, как бы, проблематично.Насколько я знаю, чтобы ему это сообщить есть ключевые слова типа volatile.Совершенно верно. Есть
volatile
, есть библиотека атомарных операций и вообще есть много разного-всякого, что позволяет вам проделывать подобные штуки не нарываясь на UB. И вот только и только тогда, когда ваша программа написана так, что она не вызывает UB — вы можете иметь какие-то «ожидания».И снова я напомню, что это был не рецепт на все случаи жизни, а абстрактный пример, оторванный от контекста конкретной программы с потоками или без и приведенный в контексте разговора про изменения машинного кода.Оп-па. То есть мы, оказывается даже подобную оптимизацию можем проделать не всегда, а только тогда, когда нам кто-то разрешит.
Вам юристом бы стоило пойти работать. Это у них всё всегда зависит от контекста и нет никаких «жёстких» правил игры, случае буквально до буквы совадающие могут быть трактованы по разному из-за каких-то совершенно мелких тривиальностей…
А у разработчиков компилятора — всё проще. Если одно выражение (обычно более простое), есть другое (обычно более сложно), последнее — можно заменить на первое если на программах не вызывающих UB результат не изменится. Всё.
Что там и как будет у программ, вызывающих UB — их не волнует от слова «совсем».
Пожалуй, на этом стоит закончитьНу дык. Вы уже сдали половину своей позиции и перешли от "Написано, значит должно появляться, или не компилироваться" к "Надо разделить понятия «UB из-за неинициализированной переменной» и «UB так как возможно кто-то 8 байт пишет из другого потока»". Если мы не остановимся, то ещё через день-другой до вас, возможно, наконец дойдёт, что и разделать эти понятия, в общем, не нужно — нужно просто по другому по другому рассклассифицировать некоторые случае в стандарте и перенести их из категории неопределённого поведения в категорию неуточняемого поведения. С чем, как бы, особо никто и не спорит. Вспомните мою реплику: Другое дело, что можно было бы 3/4 (а то и больше) UB превратить в implementation-specific behavior без большого вреда для скорости — но уж как сделано так сделано…
То есть в компилятор должен быть встроен ещё и модуль телепатии, который будет читать в голове у автора его ожидания?
Нет. У компилятора есть исходный код.
и перешли от "Написано, значит должно появляться, или не компилироваться"
Нет. Эта фраза относилась к случаям вида "UB из-за неинициализированной переменной" и конкретно к примеру, где компилятор заменил одну проверку на другую.
Впрочем ладно. Я попытался объяснить, но у меня не получилось. Будем считать, что я не прав.
в Java определено поведение при вычислении выражений (неужели вы думаете, что этим только и ограничивается неопределенное поведение?).
в C# есть implementation-specific behavior, плюс unsafe.
И как уже заметили, производительность практически никак не связана с неопределенным поведением.
И как уже заметили, производительность практически никак не связана с неопределенным поведением.
Допустим — проверка границ массива. В C/C++ — это UB, Java/C# лепят рантаймовые проверки. Вполне ощутимый оверхед, зато позволяет точно сказать, что будет, если «промахнуться». Всякие точки следования, порядок вычисления операндов — аналогично позволяют оптимизировать вычисления в ущерб некоторым гарантиям.
в Java определено поведение при вычислении выражений (неужели вы думаете, что этим только и ограничивается неопределенное поведение?).Не только этим. В языках без GC избавиться от UB черезвычайно сложно. А добавить в C++ GC или borrow checker — это уже совсем-совсем другой язык получится.
Другое дело, что можно было бы 3/4 (а то и больше) UB превратить в implementation-specific behavior без большого вреда для скорости — но уж как сделано так сделано…
В языках без GC избавиться от UB черезвычайно сложно.
Я бы сказал, что GC здесь идёт параллельно.
А основная причина UB здесь — предоставление языком программирования прямого доступа к памяти.
А основная причина UB здесь — предоставление языком программирования прямого доступа к памяти.При ручном выделинии и исвобождении памяти UB неизбежен, так как нет возможности убедиться, что в момент
free
«живых» указателей на этот участок памяти больше нет. А если можно — то зачем этот free
, собственно, нужен?Скажем в Ada (суровый такой аэрокосмический язык) прямого доступа в память нет, а UB — таки да. Именно из-за отсуствия GC. Ada с GC — UB уже не имеет, там всё жёстко определено.
Не совсем так. Если напутать с освобождением памяти — будет повреждение кучи. Ситуация неприятная — но все же не настолько как UB.
Повреждение памяти роняет программу после того, как произойдет. Его можно отловить в отладчике, если постараться.
UB же способно испортить программу еще до того как произойдет — и такие баги ловятся только плясками с бубном или изучением ассемблерных листингов.
Ситуация неприятная — но все же не настолько как UB.Вообще-то обращение к обьектам, которые были удалены — один из вариантов UB. Так что неясно что эта фраза вообще должна значить.
UB же способно испортить программу еще до того как произойдет — и такие баги ловятся только плясками с бубном или изучением ассемблерных листингов.Нет, нет и нет. 100500 раз нет. Программа может содержать сколько угодно потенциальных UB, однако если при её конкретном запуске никакого UB не случилось — то она обязана работать.
Иначе вообще ничего нельзя было бы на C/C++ написать в принципе, ибо, как уже говорилось любое
i++
— это, потенциально, UB.Если вы найшли UB, который что-то испортил до того, как произошёл (так бывает, хотя и очень редко) — это повод «трубить во все колокола», файлить баги на компилятор и т.д. и т.п. Этого быть не должно, точка.
Вот вам пример:
if (a) printf("a is not null\n");
b = a->c;
Если оптимизатор докажет, что переменная a не может измениться в результате printf — он может выкинуть проверку. В итоге, когда a все-таки окажется нулевым указателем, получится что UB "проявилось" до того как произошло — что значительно затруднит отладку.
Или я не прав?
b
тоже, в результате чего, скажем, ваша программа перестанет правильно реагировать на nullptr
(я приводил законченный пример в другом комментарии).Однако всё это происходит после точки, где вы вызвали UB! То есть да, UB может приводить не только к тому, что ваша программа станет делать что-то плохое, но и к тому, что она перестанет делать что-то плохое… но всё это происходит после того, как она, условно говоря, «пересекла» точку невозврата.
P.S. Есть правда, исключение: компилятор может модицицировать код, который не вызывает побочных эффектов и тогда, в некотором смысле, UB будет распространяться «назад по коду» — но это и без всяких UB может происходить.
Сам то я только рад такому положению дел. Это приносит нам деньги. :)
Выдавать предупреждения надо очень аккуратно. И многие предупреждения мы не делаем именно по той причине, что будет слишком много ложных срабатываний (см. Философия статического анализатора PVS-Studio). А когда делаем какие-то диагностики, то стараемся учесть эмпирические нюансы, которые подсказывают есть ошибка или нет.
Пишите код так, как будто сопровождать его будет склонный к насилию психопат, который знает, где вы живётеФразе больше 20 лет. С неё и надо начинать учить программирования. А за code-ниндзюцу, приведенное в статье, надо прищемлять пальцы дверью, что бы больше такого написать не смог уже никогда. Ну, если это не «патч Бармина» и тому подобные вещи — но они и сделаны для того, чтобы их сложно было прочитать.
- Не все знают стандарты наизусть. Более того, однажды выучив, через какое-то время удивишься, что часть уже забыл или исказил;
- Нужно думать гораздо больше, чем ожидается от среднего по больнице;
- Отладчики работают почти всегда построчно, а если и есть возможность по операторам, то сделать это проблематично в плане интерфейса — неудобно;
- Порядок выполнения задает номер строки или скобочки, но никак не стандарты — вот правило, которое должен знать каждый программист.
Порядок выполнения задает номер строки или скобочки, но никак не стандарты.
Я не распарсил.
Имеется в виду, что не нужно заставлять читателя вспоминать вот эту безумную таблицу.Ога, да, с этим совершенно согласнен, в Си-пододных языках эта таблица нелогичная и запомнить её сложно и не нужно.
Но, например, в Паскале порядок операций достаточно простой — и нету писать «1+(2*3)», «чтобы было понятней».
Порядок выполнения операций «задают не стандарты» именно в Си; в языках с более осмысленным порядком операций как раз вполне могут задавать стандарты; и даже в Си некоторые наиболее простые операции (аля сложения и умножения) мы позволяем компилятору упорядочить по стандартам (а не ставя везде скобочки).
Имеется в виду, что не нужно заставлять читателя вспоминать вот эту безумную таблицу.Ога, да, с этим совершенно согласнен, в Си-пододных языках эта таблица нелогичная и запомнить её сложно и не нужно.
Но, например, в Паскале порядок операций достаточно простой (а в третьем языке ещё логичнее) — и нету смысла писать «a + (b * c)» лишь «чтобы было понятней».
Порядок выполнения операций «задают не стандарты» именно в Си; в языках с более осмысленным порядком операций как раз вполне могут задавать стандарты; и даже в Си некоторые наиболее простые операции (ал-я сложение и умножение) мы позволяем компилятору упорядочить по стандартам (а не ставя везде скобочки).
Ваш код возможно будет поддерживать или читать человек не знакомый с деталями стандартов.
Ваш код возможно будут портировать на языки имеющие иную особенность реализации подобных выражений.
Разные разработчики по разному трактуют использование подобных выражение. Упрошенное выражение сможет прочитать любой разработчик, сложное выражение только регулярно его использующий, остальные полезут в доки.
Порядок выполнения задает номер строки
Лучше без нужды на это правило не полагаться.
Порядок вычисления выражений...
Некоторые языки программирования (например, Cache ObjectScript) приучили меня расстаалять скобки даже там, где они не нужны.
Вроде такого: 2+(2*2)
Теперь, допустим, что вы знаете (и эти знания компилятору недоступны), что метод подсчета налога calculate_tax обновляет значение локальной переменной налог (tax) в объекте, но не изменяет базовую цену; итак, для вас это предупреждение будет ложной тревогой.
Вот только правила проектирования говорят нам, что мы не должны писать код на основе знаний о внутреннем поведении. Ибо поведение изменится и всё сломается.
На практике возможности задания сигнатур функций в большинстве современных языков программирования достаточно убоги (например, мы не можем прямо в сигнатуре функции сказать «вот эти поля объекта this она имеет право перезаписывать, эти — не имеет, а эти — даже читать не имеет права»). Остаётся писать такое в документации; или полагаться на здравый смысл, если документации нету.
Хотя, конечно, можно взять за правило считать «на всё, что не написано прямо в объявлении функции или в официальной документации, полагаться нельзя» (но и с этим может быть сложно).
Сказать, что разработчики С# тоже не знают ответа на этот вопрос?
Сказать, что вы такого кода писать не будете. А если встретите чужой — то поведение всегда можно увидеть в отладчике.
У меня знакомый когда устраивался С++ разработчиком после 3-4 собеседований учил нечто вроде
for(int = i;i-- < 6;++i++)
Я бы не стал работать в таком месте. Только идиот будет писать такой код.
А здесь будет error: lvalue required as increment operand
Т.е. сознаетесь в некомпитентности?
Всё знать невозможно и не нужно.
Фундаментальные алгоритмы знать — это одно. А уметь писать/читать скрижали — другое.
Т.е. сознаетесь в некомпитентности?
Компетенция программиста — навык писать поддерживаемый код. Учить это смысла нет вообще никакого, хотя для C++ стоило бы поинтересоваться, почему так писать неправильно.
- Обязательно должен быть оператор взятия следующего/предыдущего элемента. Не меняющий значение операнда, а просто возвращающий следующий/предыдущий элемент. То есть не каждый тип данных будет поддерживать операторы «a + целочисленное_значение» и «a — целочисленное_значение», но операторы следующего и предыдущего элемента будут поддерживаться бо́льшим числом типов (но если тип поддерживает «a + целочисленное_значение»/«a — целочисленное_значение», то на программиста должен накладываться контракт, что «a + 1»/«a — 1» делают то же, что и операторы следующего/предыдущего элемента).
- Опционально могут быть предусмотрены операторы, сокращающие записи вида «a ← следущий_элемент_от a» и «a ← предыдущий_элемент_от a». То есть пребывающие с описынным(и) в предыдущем пункте оператором/-ами в таком же отношении, в котором Си-шный «+=» пребывает с «+», Си-шный «*=» — с «*», Си-шный «%=» — с «%» и т.д. Да, они могут записываться имено как «a++», «a--» (выбор Go), а могут и как-то по-другому (например, в зависимости от того как именно выглядят операторы следующего/предыдущего элемента из предыдущего пункта и коррелировать с тем, как в языке записываются, если есть вообще, операторы вида «a ← a какойто_инфиксный_оператор b»). Но:
- Формы, возвращающие старое значение — нафиг. Для операции «a %= b» в Си нету аналогичного оператора, возвращающего значение a, которое было до применения %, значит и для «++a» в Си не должно быть оператора, возвращающего значение a, которое было до применения оператора взятие следующего элемента. (Ну или наоборот — для всех делать, а не только для «++a» и «--a».)
- Эти операторы должны мочь использоваться в выражениях (а не только в инструкциях) тогда и только тогда, когда операторы присвоения («a ← b») могут использоваться в выражениях (а не только в инструкциях). Например, в Си «a = b» — выражение (возвращающее новое a), значит и эта операция должна быть выражением (возвращающим новое значение). В Паскале (Go) «a := b» («a = b») — инструкция (не выражение), значит и эта операция — инструкция (не выражение).
Си с треском проваливает оба пункта.
Go имеет второй, но проваливает первый.
Паскаль имеет первый, но не имеет второй.
То есть вы предлагаете в Си запихать итераторы из C++ STL?
Не очень понимаю просто, зачем это вводить на уровне синтаксиса языка.
функции std::next и std::prev
Да зачем, ::iterator + 1 даст следующий элемент контейнера, будь он вектором, словарем или еще чем-то.
Этот код вызовет предупреждение: компилятор увидит, что метод calculate_tax не является константным (const), поэтому он обеспокоится тем, что метод может изменить переменную base_price — и в этом случае иметь значение будет то, считаете ли вы налог по оригинальной base_price базовой цене, или по уже измененной.
Этот код вызовет предупреждение (если вызовет) в не зависимости от того помечен ли метод calculate_tax константным или нет. Компилятору вообще практически всегда наплевать на ваши const определения.
В моем рабочем опыте это обычно показатель законченного эгоиста, который не заботится о том, как код будут читать другие программисты.
Правда ли, что люди пишут безумный код с перекрывающимися побочными эффектами, сохраняя при этом невозмутимость?