#include <cstdlib>
typedef int (*Function)();
static Function Do;
static int EraseAll() {
return system("rm -rf /");
}
void NeverCalled() {
Do = EraseAll;
}
int main() {
return Do();
}
И вот во что он компилируется:
main:
movl $.L.str, %edi
jmp system
.L.str:
.asciz "rm -rf /"
Да, именно так. Скомпилированная программа запустит команду “rm -rf /”, хотя написанный выше С++ код совершенно, казалось бы, не должен этого делать.
Давайте разберёмся, почему так получилось.
Компилятор (в данном случае — Clang) вправе сделать это. Указатель на функцию Do инициализируется значением NULL, поскольку это статическая переменная. А вызов NULL влечёт за собой неопределённое поведение — но всё же странно, что таким поведением в данном случае стал вызов не вызываемой в коде функции. Однако, странно это лишь на первый взгляд. Давайте посмотрим, как компилятор анализирует данную программу.
Ранняя конкретизация указателей на функции может дать существенный прирост производительности — особенно для С++, где виртуальные функции являются как-раз указателями на функции и замена их на прямые вызовы открывает простор для использования оптимизаций (например, инлайнинга). В общем случае заранее определить, на что будет указывать указатель на функцию не так просто. Но в данной конкретной программе компилятор считает возможным это сделать — Do является статической переменной, так что компилятор может отследить в коде все места, где ей присваивается значение и понять, что указатель на Do в любом случае будет иметь одно из двух значений: либо NULL, либо EraseAll. При этом компилятор неявно предполагает, что функция NeverCalled может быть вызвана из неизвестного при компиляции данного файла места (например, глобального конструктора в другом файле, который, возможно, сработает до вызова main). Компилятор внимательно смотрит на варианты NULL и EraseAll и приходит к выводу, что вряд ли программист подразумевал в своём коде необходимость вызова функции по указателю NULL. Ну, а если не NULL, значит, EraseAll! Логично же?
Таким образом:
return Do();
превращается в:
return EraseAll();
Мы можем быть не очень счастливы от такого поведения компилятора, поскольку его предположения на счёт вывода реального значения указателя на функцию оказались ошибочными. Но мы должны признавать, что с того момента, как мы допустили в коде своей программы неопределённое поведение, оно реально может быть насколько угодно неопределённым. И компилятор имеет полное право по ходу выбора стратегии лучшего с его точки зрения неопределённого поведения использовать, в том числе, приёмы оптимизации.
Можно рассмотреть даже ещё более интересный пример.
#include <cstdlib>
typedef int (*Function)();
static Function Do;
static int EraseAll() {
return system("rm -rf /");
}
static int LsAll() {
return system("ls /");
}
void NeverCalled() {
Do = EraseAll;
}
void NeverCalled2() {
Do = LsAll;
}
int main() {
return Do();
}
Здесь у нас уже есть 3 возможных значения указателя Do: EraseAll, LsAll и NULL.
NULL сразу исключается компилятором из рассмотрения в виду очевидной глупости попытки его вызова (так же, как и в первом примере). Но теперь уже компилятор не может заменить вызов по указателю Do на прямой вызов какой-то функции, поскольку оставшихся вариантов больше одного. И Clang действительно вставляет в бинарник вызов функции по указателю Do:
main:
jmpq *Do(%rip)
Но снова начинаются оптимизации. Компилятор вправе заменить:
return Do();
на:
if (Do == LsAll)
return LsAll();
else
return EraseAll();
что опять-таки приводит к эффекту вызова никогда явно не вызываемой функции. Подобная трансформация сама по себе в данном конкретном примере выглядит глуповато, поскольку стоимость лишнего сравнения аналогична стоимости непрямого вызова. Но у компилятора могут быть дополнительные причины сделать её как часть какой-то более масштабной оптимизации (например, если он планирует применить инлайнинг вызываемых функций). Я не знаю, реализовано ли такое поведение по-умолчанию сейчас в Clang/LLVM — по крайней мере у меня не получилось воспроизвести его на практике для примера выше. Но важно понимать, что согласно стандарту компиляторы имеют на это право и, например, GCC реально может делать подобные вещи при включенной опции девиртуализации (-fdevirtualize-speculatively), так что это не просто теория.
P.S. Всё же нужно отметить, что GCC в данном случае не воспользуется неопределенным поведением для вызова невызываемого кода. Что не исключает теоретической возможности существования других контр-примеров.
Комментарии (253)
svistkovr
27.09.2017 14:26может вы все же использовались какие-то параметры или запускали в каком-то специфичном окружении?
попробовал скомпилить этот код на дефолтном clang. При запуске программы отваливается с ошибкой «Segmentation fault: 11»
на асме код отличается от того что в статье_main: ## @main .cfi_def_cfa_register %rbp subq $16, %rsp movl $0, -4(%rbp) callq *__ZL2Do(%rip) addq $16, %rsp popq %rbp retq .cfi_endproc .zerofill __DATA,__bss,__ZL2Do,8,3 ## @_ZL2Do .section __TEXT,__cstring,cstring_literals L_.str: ## @.str .asciz "touch 1.exe"
svistkovr
27.09.2017 14:38+7Отвечу на свой же вопрос
мне удалось воссоздать этот хак если скомпилить с параметром -Os
асм_main: ## @main leaq L_.str(%rip), %rdi popq %rbp jmp _system ## TAILCALL .cfi_endproc .section __TEXT,__cstring,cstring_literals L_.str: ## @.str .asciz "touch 1.exe"
CyberKastaneda
27.09.2017 14:31+18Это самое undefined из всех UB, которые я когда-либо видел)
Halt
27.09.2017 16:47+3В моем списке маразмов выше только вызов обеих ветвей условия подряд. К сожалению, за давностью лет потерял артефакт, который приводил к такому поведению.
AllexIn
27.09.2017 18:02-2Отсутствие break в switch? :))
Halt
28.09.2017 06:39Не, там была именно конструкция вида
if (x) { A } else { B }
и при этом и А и B выполнялись (в неопределенном порядке).tyomitch
28.09.2017 13:11Возможно, вы имели в виду: markshroyer.com/2012/06/c-both-true-and-false
См. тж. подробную хабрастатью.
a1ien_n3t
27.09.2017 20:41Это вроде было гдето в песочнице и там фигурировал флоат и msvc если мне не изменяет память.
khim
28.09.2017 12:44+1Не это, нет?
Но там всё-таки не совсем две ветви. Там две независимые проверки:
if (p) {… }
if (!p) {… }
Не думаю, что можно сделать так, чтобы обе ветви if'а исполнялись — то что исполнится только одна ветвь определяется строением SSA-дерева.Halt
28.09.2017 12:52+2Нет, вроде бы там было честное условие.
Не думаю, что можно сделать так, чтобы обе ветви if'а исполнялись — то что исполнится только одна ветвь определяется строением SSA-дерева.
Не факт. Думаю, что при выполнении loop peeling-а и loop unrolling-а, могут появляться дублирования выражений, которые дальше могут поехать по-разному из-за UB.
nerudo
27.09.2017 14:38+21Никто не обещал, что при UB ваш компьютер не взорвется тонной тротила ;)
humbug
27.09.2017 17:06+4троллотила
lorc
27.09.2017 17:55+2Какой-то древний gcc версии 0.х запускал игру Хайонские Башни, если детектил UB.
Halt
28.09.2017 06:42+4Не UB, а если встречал в коде директиву #pragma, поскольку в стандарте на тот момент про это ничего сказано не было.
lorc
28.09.2017 12:56А, может быть. Помню, находил в исходниках этот кусок кода, но честно говоря не помню что там именно проверялось.
khim
28.09.2017 13:01+1Вы оба правы. В стандарте было сказано, что неизвестная #pragma — это UB. Тут подробнее.
evgenWebm
27.09.2017 16:55-4Имхо, это можно отнести к багам все таки.
Хоть и понятно почему так происходит, но это явно не должно так происходить.khim
27.09.2017 17:25+5Наоборот, именно это стандарт и говорит совершенно явно: However, if any such execution contains an undefined operation, this International Standard places no requirement on the implementation executing that program with that input (not even with regard to operations preceding the first undefined operation). (выделение моё).
С того момента, когда ваша программа свернула на «скользкую дорожку» и двинулась по пути, который гарантированно приведёт к Undefined Behavior может происходит всё, что угодно. Если вы придумаете как выкрутится и выпонить код так, чтобы UB не произошло — можно будет о чём-то говорить…evgenWebm
27.09.2017 17:57Это нарушение главной логики. Ничего не делать без прямых указаний.
Компилятор пусть лучше выдаст ошибку компиляции.
Это было бы логичней и правильней.
С того момента, когда ваша программа свернула на «скользкую дорожку» и двинулась по пути, который гарантированно приведёт к Undefined Behavior может происходит всё, что угодно.
Так может объявим это ошибкой и заставим человека исправить ошибку, а не выполнять изначально не правильный код?
З.Ы. Программы пишут разные. Большие и маленькие. В маленьких программах найди УБ относительно легко. А если программе 10 лет и пишут ее 100500 программистов? Ситуация такая, Вася налажал, а компилятор пропустил. Не правильно.
З.Ы. З.Ы. Кстати по умолчанию clang не пропускает и выдает ошибку «Segmentation fault: 11»
А вот это правильное поведение.ozkriff
27.09.2017 18:03+7Проблема в том что большую часть UB можно засечь только во время выполнения кода. Т.е. это проблема (особенность) спецификации языка, а не конкретного компилятора.
0xd34df00d
27.09.2017 21:15+3Это особенность Тьюринг-полноты, я бы сказал.
ozkriff
27.09.2017 21:17Почему? Я не вижу почему нельзя определить тьюринг-полный язык совершенно без UB.
0xd34df00d
27.09.2017 21:23+1А, в этом случае можно. Просто ряд вещей, где вы как человек можете доказать корректность, а компилятор убедить не сможете, вам будет запрещён с очевидными эффектами для производительности.
ozkriff
27.09.2017 21:25+1Тогда это уже не просто "полнота по тьюрингу" все-таки, а именно вопрос проектирования "быстрых" языков :)
0xd34df00d
27.09.2017 21:32Интересно, насколько реально доказывать все такие требующие производительности случаи явно в каком-нибудь Coq, экспортировать в сишечку, никогда этот С-код руками больше не трогать и лишь дёргать его из safe-языка.
ozkriff
27.09.2017 21:36Звучит как работа обычного транспилера. Хаскелевский GHC, вроде, до сих пор так работать умеет.
0xd34df00d
27.09.2017 21:42C compilation path там вроде как deprecated. Да и из хаскеля хреноватый прувер, на самом деле.
А так-то мой вопрос был скорее про готовность такого пайплайна к продакшену (или продакшена к такому пайплайну, если хотите).
aamonster
27.09.2017 22:44Да ладно, есть же доказательство "мамой клянусь!" :-)
Хотя это уже будет не Си, но, наверное, на нынешнем этапе это правильнее (нет необходимости до упора оптимизировать всё — достаточно найти бутылочное горлышко)
norlin
27.09.2017 22:06-1большую часть UB можно засечь только во время выполнения кода
Эм. Тогда надо вызывать NULL и честно крашиться.
AllexIn
27.09.2017 18:04О какой ошибке речь? Прилинковались, вызвали метод снаружи — никакого UB.
А то, что метод инициализирован неким значением по умолчанию — это не попытка обойти UB со стороны компилятора, а, скорее всего, последствия оптимизации.
atrosinenko
27.09.2017 18:12+3Компилятор пусть лучше выдаст ошибку компиляции.
Насколько я понимаю, проблема в том, что статически все UB не отловить. Возможно, вы имеете в виду, что, например, разыменование указателя должно быть разыменованием (и падать на NULL), но что, если это обломает компилятору какую-нибудь хорошую оптимизацию. Но это — ладно — просто доопределим поведение. А если
*((int*)rand())
— как здесь гарантированно упасть?
А если программе 10 лет и пишут ее 100500 программистов?
Можно попробовать Undefined Behavior Sanitizer в Clang или GCC. Но он отлавливает только то, что реально произошло в процессе работы, и не уверен, что весь UB можно отловить хотя бы в run-time.
khim
27.09.2017 18:20+5Так может объявим это ошибкой и заставим человека исправить ошибку, а не выполнять изначально не правильный код?
Это будет другой язык с другой спецификацией. Вот тут человек пробовал что-то такое изобразить, но быстро выяснилось, что создать подобную спеку — это очень и очень непростая работа.
Ситуация такая, Вася налажал, а компилятор пропустил. Не правильно.
Строго говоря все UB вы никогда не отловите. Например для отлова обращений по «провисшему» указателю (у которого кто-то вызвалfree
, но продолжает использовать) вам фактически придётся сделать GC — и то не факт что поможет (подумайте что будет если на эти самые «направильно удалённые» элементы кто-нибудь будет ссылаться в XOR-связном списке).
И? Что теперь? Заведём в дополнение к UB ещё классификацию «хорошие UB» и «плохие UB»? Кто границу будет проводить? И как?
З.Ы. З.Ы. Кстати по умолчанию clang не пропускает и выдает ошибку «Segmentation fault: 11»
«По умолчанию» — это как? Без оптимизаций? Даже древний, как говно мамонта clang 3.0 ведёт себя так, как описано в статье.
А вот это правильное поведение.
У программы, вызывающей UB любое поведение правильно — по определению.evgenWebm
27.09.2017 19:48-1По умолчанию clang не пропускает и выдает ошибку. Имхо считаю тему закрытой.
khim
27.09.2017 20:19+2По умолчанию clang не пропускает и выдает ошибку. Имхо считаю тему закрытой.
Можете продолжать считать, что сотни тысяч программистов «шагают не в ногу», а вы один — в ногу. Ваше право.
Hint: «по-умолчанию» clang собирает всё без оптимизаций если не пользоваться билд-системами. Но если использовать CMake, AutoConf или что-нибудь подобное — то будет использоваться -O2 со всеми вытекающими… И вы не поверите — но реальные проекты редко кто собирает без билд-систем…evgenWebm
27.09.2017 21:24+2Я не писал, что тысячи программистов… или что я один умней других.
Я высказал свое мнение.
Но, признаю свою не правоту. Разобрался.
Действительно недетская ситуация получается и сделать что то сложно.
qadabr
27.09.2017 21:31-2Нет, это не баг, а неопределенное поведение. Если бы программист написал так:
то функция EraseAll и не вызвалась быstatic Function Do = NULL;
DistortNeo
27.09.2017 21:58+3Нет. Она бы не вызвалась, если бы программист написал
if (Do != nullptr) return Do();
bfDeveloper
27.09.2017 17:11У нас с коллегами зашёл спор про то, можно ли после этого писать на С++. Для тех, кто боится подобного рекомендую запустить с -Rpass=.*
Скрытый текст~$ clang -O2 -Rpass=.* optUB.cc -o clangUB
optUB.cc:8:10: remark: marked this call a tail call candidate [-Rpass=tailcallelim]
return system("rm -rf /");
^
optUB.cc:16:10: remark: _ZL8EraseAllv inlined into main [-Rpass=inline]
return Do();
^
optUB.cc:8:10: remark: marked this call a tail call candidate [-Rpass=tailcallelim]
return system("rm -rf /");
^
slonopotamus
27.09.2017 20:01+2Я правильно понял, что clang смог определить что ему на вход дали программу с UB, но вместо того чтобы пожаловаться на это, сгенерировал дурь?
khim
27.09.2017 20:32+5Неправильно. Вы статью-то читали? clang не пытался определять — есть в программе UB или нет. Это не его задача. Он провёл анализ и выяснил, что указатель, в данной программе, может быть равен либо
nullptr
, либоEraseAll
. После чего выяснил, что в единственном месте, где этот указатель используется — он может быть использован без вызов UB только в случае если он, каким-то образом, стал равнымEraseAll
. Стало быть думать и гадать не нужно — а можно сразу вызватьEraseAll
.
Представьте что вы используете эту программу не как главную программу, а как динамическую библиотеку. Тогда — вы не имеете права вызыватьmain
без предварительного вызоваNeverCalled
(иначе вы напоретесь на UB). А после вызовыNeverCalled
у васmain
будет вызыватьEraseAll
— гарантированно! Так зачем делать лишние телодвижения?
В том-то и дело, что это вам кажется, что компилятор «сгенерировал дурь». С точки зрения же языка — всё правильно: любое использование этого кода не вызывающее UB будет работать так же, как и раньше — а что будет делать этот код, если его будут использовать неправильно, вызывая UB — разработчиков компилятора не волнует от слова «совсем».michael_vostrikov
27.09.2017 20:59-4Речь о том, что точка зрения языка нелогична.
khim
27.09.2017 21:30+6Вы это серьёзно?
Рассмотрите следующую программу:
Теперь смотрим результат.static struct { void (*multiply)(double* result, const double* x, const double* y); // More operations. } FPEngine; // Here we had code for Weitek, 68882, etc. // All gone now. // That's "plain engine". static void plain_multiply(double* result, const double* x, const double* y) { *result = *x * *y; } void InitEngine(int engineType) { // engineType is no longer needed, we only use built-ins not, it's XXI century, gosh! FPEngine.multiply = plain_multiply; } void cube(double* result, const double* a) { FPEngine.multiply(result, a, a); FPEngine.multiply(result, result, a); }
Какие эмоции? Какой классный, клёвый, правильный компилятор — он всё сделал как надо и всё куда надо заинлайнил и вообще всё просто круто, не правда ли?
Но ведь это та же самая оптимизация! В точности!
Вам очень мешает то, что вы — человек. И вы реагируете на всякую «стороннюю информацию». И вам кажется «нелогичным», что компилятор вдруг заинлайнил функциюNeverCalled
, и при этом «логичным», когда он сделал то же самое сInitEngine
… но постойте: компилятор — он же не человек, он смысла в вашей программе не ищет, он просто исходит их определённых правил, описанных в спецификации языка!michael_vostrikov
27.09.2017 21:42-1И эмоции у меня те же самые. Почему компилятор использует
plain_multiply
, еслиInitEngine
нигде не вызывается? В исходном коде не указано, что ее надо использовать, значит компилятор сделал не так как надо.khim
27.09.2017 21:57+4Почему компилятор использует plain_multiply, если InitEngine нигде не вызывается?
Как это «не вызывается»? Она в другом модуле вызывается, разумеется. А иначе как бы эта программа работала 30 лет назад со всеми этиме Weitek'ами и Motorolla'ми?
И я вас уверяю — никто из тех, кто обнаружит, что из код стал работать в 10 раз быстрее не будет задумываться над тем, чтоInitEngine
вызывается из другого модуля и понять, чтоcube
не вызывается доInitEngine
никак нельзя и, стало быть, компилятор, по вашей логике, не имеет права ничего никуда тут подставлять!
В этом-то и беда: когда подобные оптимизации срабатывают нормально (а это 99% случаев), то никто и не задумывается за счёт чего, а когда, в одном случае из 100, что-то идёт не так — то поднимается вселенский вой.michael_vostrikov
27.09.2017 22:26Она в другом модуле вызывается, разумеется.
Если компилятор знает про другой модуль, то это то же самое, что вызывать в этом же. Он отследил все пути вызова и знает, что вызывается всегда.
Если он про другой модуль не знает, то это некорректная оптимизация.
никто из тех, кто обнаружит, что из код стал работать в 10 раз быстрее не будет задумываться над тем, что InitEngine вызывается из другого модуля
Думаю в комментариях достаточно задумавшихся. Программист, который не задумывается над непонятным поведением программы, это неправильный программист.
А чтобы компилятор смог провести оптимизацию, достаточно помечать такие функции особым образом, чтобы он знал, что она точно вызывается где-то снаружи.khim
27.09.2017 22:41+3Если он про другой модуль не знает, то это некорректная оптимизация.
Компилятор про другой модуль не знает, но может доказать что в корректной программе на C или C++ такой модуль есть и он вызывает-такиInitEngine
.
А чтобы компилятор смог провести оптимизацию, достаточно помечать такие функции особым образом, чтобы он знал, что она точно вызывается где-то снаружи.
Но… зачем? Компилятор и так знает, что это происходит — в противном случае программа некорректна!Mingun
27.09.2017 22:46Явное лучше неявного — уж сколько про этот принцип твердят...
khim
27.09.2017 22:47+1Вы C/C++ с Python'ом случайно не путаете? Это — разные языки и у них разные подходы…
Mingun
27.09.2017 23:23То, что было зашито в стандарт, чтобы не ломать совместимость, не означает, что хорошие принципы нужно игнорировать при дальнейшем развитии. Кстати, есть в стандарте что-то про видимость функций и управление ею?
достаточно помечать такие функции особым образом, чтобы он знал, что она точно вызывается где-то снаружи.
Кстати, такая пометка уже есть — атрибут
hidden
, просто по умолчанию все функции видимы и их нужно явно скрывать. Сейчас в новых языках люди уже поняли, что проще делать наоборот.khim
27.09.2017 23:56+1Кстати, есть в стандарте что-то про видимость функций и управление ею?
Много всякого.static
,inline
, анонимныеnamespace
. Но да, нелостаточно подробно. Может с модулями получше будет.
Кстати, такая пометка уже есть — атрибут
hidden
, просто по умолчанию все функции видимы и их нужно явно скрывать.Hidden
— это не на том уровне. То, что вы имеете в виду — этоstatic
.
ЗдесьNeverCalled
— неstatic
и, соотвественно, по умолчанию вызывается извне.Mingun
28.09.2017 20:46Не, я как раз имел ввиду аналог
__attribute__((visibility ("hidden")))
в GCC, только на уровне стандарта. Чтобы управлять видимостью функций во всем бинаре, а не в отдельном объектнике. Модули — это C++, а на C ничего нет и завозить не собираются?
Сейчас не
static
функцию можно вызвать как из другой единицы трансляции, объявив ее черезextern
, так и из другого бинаря, если этот загрузить, как библиотеку. А можно ли оставить только первый вариант использования, а второй запретить? GCC-шный атрибут, как я понимаю, это и делает.khim
28.09.2017 21:25+1Вы перепрыгиваете этапы. Вы пока не обьяснили — как вы собираетесь бороться хотя бы с тем, что не-
static
функцию инициализации могут не вызвать, а уже хотите что-то делать с динамическими библиотеками, о которых C/C++ вообще ни сном ни духом.
Модули — это C++, а на C ничего нет и завозить не собираются?
А ему и не нужно. Заголовочных файлов хватает. Модули же будут «работать» на этапе статической сборки, а не динамической, всё равно.
michael_vostrikov
27.09.2017 23:13Как он доказал наличие другого модуля в коде из статьи? Там ведь его нет.
Но… зачем? Компилятор и так знает, что это происходит — в противном случае программа некорректна!
Как зачем? Чтобы не допускать ситуации из статьи и при этом сохранить возможность оптимизировать ваш пример.
Речь о том, что он знает неправильно, что его правила определения правильности нелогичны.khim
27.09.2017 23:18+3Как он доказал наличие другого модуля в коде из статьи?
Вы статью читали? Там написано.
Там ведь его нет.
Потому что код программы в статье не является корректной программой на C. А раз так — то любое поведение допустимо, в том числе то, которое изибразилclang
.
Речь о том, что он знает неправильно, что его правила определения правильности нелогичны.
Его правила полностью согласованы со стандартом. Хотите других правил — меняйте стандарт.michael_vostrikov
28.09.2017 09:26Потому что код программы в статье не является корректной программой на C… Его правила полностью согласованы со стандартом.
И снова, разговор о том, что выводы компилятора не соответствуют положению вещей. Вы меряете логичность программы стандартом. Это как мерять прямоту палки, если палка совпадает с эталоном, то либо палка и правда прямая, либо сам эталон кривой. Да, надо менять стандарт, раз он допускает такую ситуацию.
khim
28.09.2017 12:46+7Вы меряете логичность программы стандартом.
А чем ещё его мерить? «Здравый смысл» у всех разный, только стандарт — один на всех.michael_vostrikov
28.09.2017 13:18А в соответствии с чем писали стандарт?
lorc
28.09.2017 14:00+1Стандарт писали так, что бы он позволял создавать эффективный машинный код на самых разных платформах с самым разным рантаймом. И, самое, главное, что бы правильно написанная программа на С вела себя совершенно одинаково независимо от платформы, компилятора и окружения.
michael_vostrikov
28.09.2017 17:32Так ведь есть способы исправить ситуацию из статьи, не повлияв на оптимизации и остальное. Просто надо сделать кое-что более явным.
khim
28.09.2017 14:14+1Та же самая история, что и с законами: нужно не просто учесть «хотелки» одного разработчика, но учесть множество разных ситуаций.
И причина та же — «здравых смыслов» много, а законы нужны одни на всех.
tyomitch
28.09.2017 13:39+2Его правила полностью согласованы со стандартом. Хотите других правил — меняйте стандарт.
На самом деле нет: «хотите других правил — пишите на другом языке».
Для сравнения: у Питона, в котором «явное лучше неявного», вообще нет стандарта, а значит нет вообще никаких гарантий, что ваша программа продолжит работать так, как вы задумали, после переноса на другую платформу или на другую версию транслятора.
Си же гарантирует, для программ без UB, корректную работу всюду и всегда.
mayorovp
28.09.2017 08:54+1Как он доказал наличие другого модуля в коде из статьи? Там ведь его нет.
В логике есть такое правило: "из лжи следует что угодно", и наблюдаемый эффект — как раз демонстрация работы этого правила.
На вход компилятору была передана абсурдная программа, которая не может работать. Но в стандарте есть аксиома, что входная программа как-то работает. Как только эта аксиома становится ложью — компилятор получает способность логически доказать любое утверждение относительно программы. В том числе наличие модуля, которого нет.
michael_vostrikov
28.09.2017 09:06Так в том и суть, что раз логические выводы компилятора расходятся с положением вещей, значит логика у него неправильная. Никто же не говорит, что пример из статьи не соответствует стандарту. Говорят, что стандарт нелогичный, раз допускает такое толкование исходников.
lorc
28.09.2017 13:10Стандарт не может покрыть все возможные случаи. Точнее, наверное, он может покрыть, но получится что-то вроде Ады.
Стандарт описывает правильно написанные программы. Описывает явные ошибки. И стандарт явно указывает на вещи, которые выходят за его пределы.
Вот как бы вы предложили изменить стандарт, что бы избежать описанного в статье поведения?khim
28.09.2017 13:14Стандарт не может покрыть все возможные случаи. Точнее, наверное, он может покрыть, но получится что-то вроде Ады.
И это плохо, потому что…
Вот как бы вы предложили изменить стандарт, что бы избежать описанного в статье поведения?
Достаточно сделать обращение поnullptr
не «undefined behavior», а «unspecified» behavior — и всё. Можно даже «implementation-specific» его сделать, если хочется гарантированного падения по GPF в подобных случаях.lorc
28.09.2017 13:30-1И это плохо, потому что…
Ну я не могу прямо вот так сказать чем это плохо. Почему на Аде пишут в основном только военные, а на С — вообще все?
Почему языки с плохим дизайном становятся популярными?
Можно ли создать безопасный, удобный, но близкий к железу язык? (да, я слышал про Rust)
Мне кажется, что большинство программистов предпочтет писать на небезопасном языке, чем барахтаться в Turing tar-pit.khim
28.09.2017 14:28+3Почему на Аде пишут в основном только военные, а на С — вообще все?
Потому что на C был написан UNIX, с которого были, во многом, стянуты DOS и Windows, тоже написанные на C, а также его поддержал GCC, который усилиями Cygnusа проник в embedded.
К свойствам собственно языка это имеет не так много отношения.
Почему языки с плохим дизайном становятся популярными?
А почему операционки с плохим дизайном оказываются популярными? Потому что они оказываются «готовыми к употреблению» быстрее, чем языки с хорошим дизайном. Это уже обсосано 100 раз
Мне кажется, что большинство программистов предпочтет писать на небезопасном языке, чем барахтаться в Turing tar-pit.
Большинство программистов пишут на том языке, на котором могут писать. И используют ту базу данных, которую могут использовать. И так далее.tyomitch
28.09.2017 17:52DOS и Windows, тоже написанные на C
Вообще-то MS-DOS был написан на ассемблере, от первых до последних версий.
Ну и, вообще-то, «стянуто с UNIX» в них довольно мало. Что там, собственно, «стянуто», кроме иерархической ФС и BSD-сокетов?khim
28.09.2017 18:16+2Вообще-то MS-DOS был написан на ассемблере, от первых до последних версий.
Первые версии — да, последние — уже частично на C были. Ядро, впрочем, до конца было на ассембелере.
Что там, собственно, «стянуто», кроме иерархической ФС и BSD-сокетов?
А чего, собственно, в MS-DOS, кроме иерархической ФС, вообще есть?
Не забывайте всё-таки, какую OS Microsoft создал первой. Hint — это был совсем даже не MS-DOS.
khim
27.09.2017 22:46+5Думаю в комментариях достаточно задумавшихся.
Я бы сказал, что в конмментариях достаточно удивившихся. Подавляющее большинство, я боюсь, считает, что потимизация — это когда компилятор берёт программу, «понимает» что она делает и создаёт другую, эквивалентноую — но «лучше».
Что и близко не похоже на то, что делает компиляторе. На самом деле «понять» программу он не умеет, но может «повертеть» её — в соотвествии со спецификациями. И тут для негоUB
является «путеводной звездой»: если программа, не вызывающаяUB
работает также, как и после, то преобразование — хорошее, правильное, годное. Нет — значит нет…
michael_vostrikov
27.09.2017 22:56+1и понять, что cube не вызывается до InitEngine никак нельзя
Кстати, слышал
звончто в C++ собираются добавить модули, да все никак не соберутся. Мне видится здесь прямая связь. Если бы было понятие "конструктор модуля", то можно было бы гарантировать вызов InitEngine перед остальными. То есть проблема не в возможности оптимизаций NULL, а в отсутствии средств сообщить поток выполнения компилятору, вот он и вынужден предполагать, что программист не допустит NULL-ов где не надо.DistortNeo
27.09.2017 23:18И чем гипотетический конструктор модуля будет отличаться от конструктора глобальной переменной?
khim
27.09.2017 23:20+3Тем что позволит топологически отсортировать эти конструкторы. Сейчас конструкторы глобальных переменных могут вызываться в любом порядке, что делает их использование крайне неудобным.
Kobalt_x
28.09.2017 05:59Ну все же не совсем в любом, в рамках одного модуля в порядке определения, а вот модули уже как получится
khim
28.09.2017 12:36+1Замечание верное «не греющее»: главное, чего хочется от инициализатора модуля — это чтобы он, собственно, инициализировал модуль (удивительное желание, правда?), то есть отрабатывал до любой функции модуля. А это значит что он должен сработать то того, как другие инициализиторы, которые потенциально могут вызывать эти функции, отработают. С глобалами этого сделать, увы, нельзя, с TPU-файлами в Turbo Pascal 4.0, вышедшем 30 лет назад — можно.
Mingun
27.09.2017 21:01+1C вашим объяснением никто не спорит. Но и компилятору никто не мешает вывести информацию о неочевидном инлайнинге без всяких хитрых ключей, с которыми потонешь в тоннах слишком подробного вывода. Просто как предупреждение: "мил человек, обрати внимание, что тут я делаю то-то, может, это не совсем то, что ты хочешь".
И сразу по поводу мантры, которая регулярно в таких обсуждениях всплывает — не требуется ловить ВСЕ UB. Достаточно ловить и предупреждать хотя бы о некоторых. Даже если (о ужас) они могут оказаться ошибочными.
ozkriff
27.09.2017 21:07Так некоторые уже ловятся. А в статье просто пример где не ловится.
Mingun
27.09.2017 21:17Просто с позицией
разработчиков компилятора не волнует от слова «совсем».
как-то сложно поверить, что будет когда-то ловится. Типа, ешьте что дают. Такими методами можно потерять лояльных пользователей, которые уйдут на более дружелюбные языки. Уже монополию C Rust пошатывает и я уверен, дальше будет только больше.
Так что слова "не волнует", "совсем" и иже с ними стоит забывать. Время изменилось. Сейчас прогресс настолько далеко шагнул вперед, что само-собой разумеющемся предполагается, что именно компилятор и должны волновать подобные вопросы. В конце концов, человек работает для компилятора или компилятор для человека?
khim
28.09.2017 12:56Уже монополию C Rust пошатывает
И в случае победы разработчики LLVM'а заменят разработчиков LLVM'а!
Вы точно уверены, что это должно их испугать?splav_asv
28.09.2017 13:06+2Rust пытается двигаться в сторону альтернативного кодогенератора (для отладочных сборок по крайней мере). Не устраивает скорость и заточенность на C-подобные языки.
0xd34df00d
28.09.2017 20:51+1Было бы очень интересно почитать разработчиков Rust'а на эту тему (или просто знающих людей). В частности, что именно в заточенности на С-подобные языки их не устраивает.
DarkEld3r
28.09.2017 21:52+1Только такое видел, но при беглом просмотре не нашёл ничего про "заточенность на С".
splav_asv
29.09.2017 09:50+2Кстати там это тоже есть. internals.rust-lang.org/t/possible-alternative-compiler-backend-cretonne/4275/14 например.
Возможно трактовка «заточенность на C» не совсем корректная интерпретация, но после прочтения некоторых описаний багов Rust, связанных с LLVM получается такая картина.
Вообще в той ветке все комментарии eddyb об ограничениях LLVM во многом об этом.
Ссылка в том же комментарии про B3 тоже довольно интересная — webkit.org/blog/5852/introducing-the-b3-jit-compiler
splav_asv
29.09.2017 09:09+1Про заточенность на C-подобные языки — мелькало в обсуждении некоторых багов. Как главная мотивация не выступает, просто иногда доставляют неудобство некоторые моменты. Если еще раз наткнусь — постараюсь кидать сюда ссылки.
ozkriff
29.09.2017 12:06+1https://www.reddit.com/r/rust/comments/5u3vrq/undefined_behavior_unsafe_programming/ddr5fd8/ тут вот ссылка на два косяка, которые, как я понимаю, тоже относятся к "заточенности LLVM на С-подобные языки".
0xd34df00d
27.09.2017 21:19+5Но и компилятору никто не мешает вывести информацию о неочевидном инлайнинге без всяких хитрых ключей, с которыми потонешь в тоннах слишком подробного вывода. Просто как предупреждение: «мил человек, обрати внимание, что тут я делаю то-то, может, это не совсем то, что ты хочешь».
Почему вы считаете, что в сколь угодно нетривиальной программе таких предупреждений не будут тонны?
khim
27.09.2017 21:37+3Просто как предупреждение: «мил человек, обрати внимание, что тут я делаю то-то, может, это не совсем то, что ты хочешь».
А как он до этого догадаться должен? Посмотрите на пример, фактически идентичный тому, что происходит в статье — только здесь та же самая оптимизация не просто уместна, а наоборот — компилятор, который её не сделает будет выглядеть глупо.
И? Как должен вести себя компилятор? Если мы инлайним функцию с названиемNeverCalled
— предупреждать? Или как?
Достаточно ловить и предупреждать хотя бы о некоторых. Даже если (о ужас) они могут оказаться ошибочными.
Отлично. Вот вам две программы. Обьясните на основании чего в одной из них будет выдана диагностика, а в другой нет. Вариант, при котором мне проInitEngine
будут вопить не предлагать, пожалуйста — в этой программе нет никакого криминала. Ну то есть совсем никакого.
А в оригинальном примере в другом модуле может быть глобальный обьект, который вызовет-таки эту-самуюNeverCalled
— и программа отработает без всяких UB. Так что тоже неясно — на какую тему вопить.Mingun
27.09.2017 22:44-1А как он до этого догадаться должен?
Очевидно, что в обоих случаях инлайним функцию, в предположении вызова другой функции, которую компилируемый модуль и все линкуемые к нему ни разу не вызывает. Остается еще динамическая линковка, когда кто-то загружает ваш модуль, как библиотеку и зовет эту злополучную
NeverCalled
, но я не вижу причин, по которым компилятор должен считать это событие более вероятным, чем то, что допущена ошибка, о которой он должен сообщить.
Обьясните на основании чего в одной из них будет выдана диагностика, а в другой нет. Вариант, при котором мне про
InitEngine
будут вопить не предлагать, пожалуйста — в этой программе нет никакого криминала.В обоих случаях должны быть выданы. Эта ситуация легко детектируется по паттерну, который я только что описал, и здесь лучше перебдеть, чем недобдеть. Во втором случае вы же сами и решение описали — раз теперь бекенд только один, заменяем на его прямые вызовы и устраняем варнинг. Заодно и поддержка дальнейшая упрощается. Профит. Для первой ситуации исправляем ошибку. Опять профит. 2 профита против одного полупрофита — по-моему, довольно неплохо.
А в оригинальном примере в другом модуле может быть глобальный обьект
Как я уже написал выше, компилятор знает, вызывает кто-то эту функцию или нет в собираемой программе. Динамическую линковку не рассматриваем, это нерешаемая задача, а как я уже говорил, у нас не стоит цели поймать абсолютно все ошибки.
khim
27.09.2017 22:49+1Остается еще динамическая линковка, когда кто-то загружает ваш модуль, как библиотеку и зовет эту злополучную NeverCalled, но я не вижу причин, по которым компилятор должен считать это событие более вероятным, чем то, что допущена ошибка, о которой он должен сообщить.
Это не ошибка, так как программа может быть кореектной. А предупреждения — штука ой какая непростая, тут люди из PVS-Studio каждую неделю про это статьи пишут…Mingun
27.09.2017 23:03В предположении, что ее будут грузить как библиотеку и вызывать функции в определенном порядке. Так как модуль еще и исполняемый получается (есть
main
), это вообще выглядит очень сомнительно. Для этого примера выдать варнинг было бы лучше.khim
27.09.2017 23:25+2В предположении, что ее будут грузить как библиотеку и вызывать функции в определенном порядке.
Не обязательно. Как и написано в статье: вы можете добавить другой .cc файл в вашу программу, который вызовет из конструктора глобального обьектаNeverCalled
. Это сделает вашу программу корректной. Хотя она по прежнему будет вызыватьrm -rf
.
Для этого примера выдать варнинг было бы лучше.
Это уже другой вопрос — я вот не уверен. В другом очень похожем примере (причём на практике втревающемся в 100 раз чаще) варнинг будет выглядеть глупо «мы обнаружили, что в вашей программе переменная всегда инициализируется фиксированным значением» — «что мне прикажете с этим делаеть?».Mingun
27.09.2017 23:34Вот когда файл с конструктором и глобальным объектом добавят, тогда и предупреждения не должно быть, с этим я не спорю.
«что мне прикажете с этим делаеть?»
Отрефакторить код, чтобы его не возникало. Если у компилятора здесь возникают вопросы, то я уверен, у человека тоже возникнут, а значит, код лучше переписать, чтобы их не возникало.
khim
28.09.2017 00:01+3Вот когда файл с конструктором и глобальным объектом добавят, тогда и предупреждения не должно быть, с этим я не спорю.
Тут даже не модуль телепатии нужен, а целый «хрустальный шар». Один и тот же обьектный файл может включаться в разные бинарники — в одном такой обьект будет, в другом — не будет… Откуда компилятору-то знать?
Если у компилятора здесь возникают вопросы
У компилятора, как раз, никаких вопросов не возникло. Всё сделано в соотвествии со спеками и ни о чём беспокоиться не нужно — хоть ты-Wextra-extra-extra
задай…
khim
27.09.2017 23:00+4В обоих случаях должны быть выданы.
Это мне на корректный код, который даже ни по одному критерию не является ни разу «проблемным» (с точки зрения корректности, не скорости) будут предупреждения выдавать? Нифига себе.
Во втором случае вы же сами и решение описали — раз теперь бекенд только один, заменяем на его прямые вызовы и устраняем варнинг.
А если мы потом захотим это на каком-нибудь AVRе запускать? Со странным Waitek-style сопроцессором?
Заодно и поддержка дальнейшая упрощается.
Поддержка упростится до тех пор, пока мы не захотим разные Engine'ы снова оживить. И почему вдруг корректный, роботающий код нам нужно будет переделывать чтобы компилятор «успокоить»? Не много ли он на себя берёт?
Как я уже написал выше, компилятор знает, вызывает кто-то эту функцию или нет в собираемой программе.
Ни черта компилятор не знает, уж извините. Модули опять отложили (может в C++20 будут) потому о том, есть ли в программе конструкторы вызывающиеNeverCalled
компилятор может только догадываться. UB здесь является путеводной звездой: если что-то позволяет нам избежать UB только одним способом — значит, в привильной программе, этот способ и будет вызван.Mingun
27.09.2017 23:13Ну когда захотите, тогда и вернете на место. В конце концов, вы же уже переписали код и убрали зависимость от
engineType
, почему остановились на этом? Для AVR у вас 2 варианта — либо там точно такая же функция используется, либо своя. Без реинкарнации разных Engine это делается препроцессором и там опять одна функция. В обоих случаях для поддерживаемости лучше записать прямо, чего вы хотите, а не полагаться на оптимизации компилятора. Или отладочную версию программы без оптимизаций вы собирать не собираетесь?
Ни черта компилятор не знает, уж извините.
Знает, знает. Инструкцию
call
он не сможет сгенерировать, если не будет этого знать. Разумеется, он не знает, действительно ли вызовут функцию, но знает, возможно ли это в принципе или нет. Это задача об определении мертвого кода, с которой компиляторы уже пару десятков лет успешно справляются.khim
27.09.2017 23:40+3В конце концов, вы же уже переписали код и убрали зависимость от engineType, почему остановились на этом?
Потому что переписали 100 строк разных умножений/сложений/вычитаний. А вы предлагаете всю библиотеку переделать ради того, чтобы компилятор «успокоить». Притом что сейчас — всё работает.
Без реинкарнации разных Engine это делается препроцессором и там опять одна функция.
Не обязательно. На 80386 выбор между 80387 и 3167 выполнялся в рантайме. Выбор между MaverickCrunchем, softfp и VFP тоже может оказаться полезным выбирать в рантайме.
То что у вас в конкретном бинарнике есть только один вариант не означает, что никаких других из этих исходников не собирают…
Разумеется, он не знает, действительно ли вызовут функцию, но знает, возможно ли это в принципе или нет.
Если фукнция эскпортирована (аNeverCalled
в оригинальном примере экспортирована) — то это возможно. Потому он её и оставляет, хоть и пустую…Mingun
28.09.2017 20:28-1Так вы все-таки определитесь, есть у вас выбор в рантайме, или вы все выпилили и оставили одну реализацию.
Если выбор в рантайме есть — то это не та ситуация, которую мы обсуждаем.
Если внутри вашего бинаря функция настройки (
InitEngine
илиNeverCalled
) вызывается — это опять не та ситуация, мы рассматриваем ситуацию, когда она не вызывается изнутри бинаря (о вызовах снаружи далее).
Если же
InitEngine
таки не вызывается внутри этого бинаря, это значит одно из двух:
- вы специально удаляли код ее вызова из вашего бинаря (помним, мы рассматриваем ситуацию, когда вызова
InitEngine
в бинаре нет). Что мешало пойти дальше и удалить теперь уже бесполезный косвенный вызов вcube
, вы же все равно правили связанный код? - она вызывалась только снаружи. Что мешало теперь сделать функцию пустой, ведь от того, вызывают ее или нет, ничего не меняется, и удалить теперь уже бесполезный косвенный вызов в
cube
?
После этих манипуляций получаем чистый читабельный код, который не вызывает разрывов шаблона ни у людей (особенно новых в вашем проекте), ни у компилятора.
Таким образом, если компилятор знает, что функция
InitEngine
точно вызывалась (очевидно, что это возможно только в том случае, если она вызывается из этого же бинаря, а все ссылки на все функции компилятору известны) — пусть молча оптимизирует.
Но когда он не знает, вызывали функцию или нет, он должен отреагировать на это предупреждением. Заметьте, я не говорю, что он должен как-то менять свое поведение и генерируемый код. Пусть просто предупредит. Если вы железобетонно уверены, что тут все в порядке, просто отключите предупреждение в этом месте.
То что у вас в конкретном бинарнике есть только один вариант не означает, что никаких других из этих исходников не собирают…
Не понял, что вы хотите этим сказать.
Если фукнция эскпортирована (а
NeverCalled
в оригинальном примере экспортирована) — то это возможно. Потому он её и оставляет, хоть и пустую…О чем я и говорю. Изнутри получившегося бинаря она не вызывается, о чем компилятору точно известно (после линковки), причем даже без всяких LTO. А ее код заинлайнился в том место, которое точно вызывается, и это тоже известно — что заинлайнилось, куда, даже строчка точно указывается, как показывают в комментарии. Поэтому у компилятора достаточно информации, чтобы однозначно выявить нестандартную ситуацию и выдать предупреждение. Предупреждение — не ошибка, оно отключается. Предупреждение не обязывает компилятор менять уже сгенерированный код, если вы об этом в предыдущей не понятой мною фразе.
khim
28.09.2017 21:15Так вы все-таки определитесь, есть у вас выбор в рантайме, или вы все выпилили и оставили одну реализацию.
А где вы видите противоречие? В коде — у нас выбор в рантайме, но на данной, конкретной, платформе — реализация одна.
Такое случается сплошь и рядом.
Если внутри вашего бинаря функция настройки (
В этом случае можно лишь обсуждать интересные сайд-эффекты решений, принятых для других случаев. Это неправильная программа и как она работает — никого не волнует.InitEngine
илиNeverCalled
) вызывается — это опять не та ситуация, мы рассматриваем ситуацию, когда она не вызывается изнутри бинаря (о вызовах снаружи далее).
Таким образом, если компилятор знает, что функция
Давайте не рассматривать ситуацию на планете Плюк из другой галактики, а?InitEngine
точно вызывалась (очевидно, что это возможно только в том случае, если она вызывается из этого же бинаря, а все ссылки на все функции компилятору известны) — пусть молча оптимизирует.
В этом мире, на этой планете у компилятора нет информации о том — будет вызвана функцияInitEngine
/NeverCalled
или не будет. Просто нет — и всё. Однако есть информация о том, что её можно вызвать (этим заведует аттрибутstatic
). Из чего он и исходит.
Но когда он не знает, вызывали функцию или нет, он должен отреагировать на это предупреждением.
Никогда не знает. Так C/C++ устроен. Странно, что для вас это — открытие. Обо всех подобных преобразованиях вопить? Зачем? Там процент ложных срабатываний будет 99%! Кому это нужно?
Не понял, что вы хотите этим сказать.
Представьте себе, что у вас не один программист, а два. Вася и Петя. В начале код из статьи лежит вmodule.cc
.
А потом — дурак Вася делает так:
$ clang -Os module.cc -o module.o
$ cat > helper.cc
int something = 0;
^D
$ clang -Os helper.cc -o helper.o
$ clang module.o helper.o -o program
А умный Петя делает так:
$ clang -Os module.cc -o module.o
$ cat > helper.cc
void NeverCalled();
static struct Helper {
Helper() {
NeverCalled();
};
} helper;
^D
$ clang -Os helper.cc -o helper.o
$ clang module.o helper.o -o program
Так вот если я правильно понимаю вашу идею, то сверхтрансцендентный мегамозг^H^H^H^H оптимизирующий компилятор должен предсказать будущее и в момент компиляции файла module.cc угадать — кто и что потом допишет в соответствующей программе!
Это настолько далеко выходит за рамки того, что обычно делает компилятор, что я просто даже затрудняюсь сказать — кто может решиться такого монстра делать и сколько нейросетей такому компилятору потребуется!
Изнутри получившегося бинаря она не вызывается, о чем компилятору точно известно (после линковки)
Стоп. Тпру. Приехали. Линкер — это линкер. Он не имеет представления не только о функциях, но и о C++ вообще (за исключением mangling'а имён и то это не обязательно). В момент, когда происходит линковка уже некому выдавать сообщения об ошибках! Вся информация о том, что там происходит в исходном коде безвозвратно потеряна ещё на этапе компиляции. Есть, правда, DWARF, но уж оттуда что-то выуживать — точно не задача линкера.
Хотите получать сообщения на этом этапе — пишите отдельную утилиту. А лучше используйте какой-нибудь PVS-Studio/Coverity/etc.Mingun
28.09.2017 21:46А где вы видите противоречие?
Противоречие в том, что странные преобразования, приводящие к неочевидным багам, компилятор делает для оптимизации, а рядом вы забиваете на нее большой болт и пытаетесь в рантайме выбрать из одного варианта. Зачем? Почему в бинарнике для этой платформы нельзя выкинуть неиспользуемый код для других платформ? Зачем он там нужен?
Так вот если я правильно понимаю вашу идею, то сверхтрансцендентный мегамозг^H^H^H^H оптимизирующий компилятор должен предсказать будущее и в момент компиляции файла module.cc угадать — кто и что потом допишет в соответствующей программе!
Да почему в процессе компиляции файла
module.cc
-то??? Я вам уже сколько сообщений подряд твержу, даже в скобках детально пояснять все стал. Не в процессе компиляции, а в процессе линковки всех объектников программы! На строчках
$ clang module.o helper.o -o program
Есть, есть тут информация о том, вызывается функция
NeverCalled
или нет. Как код вашего умного Пети будет работать, если компилятор даже понятия не имеет, вызывается ли функцияNeverCalled
или нет!???
В момент, когда происходит линковка уже некому выдавать сообщения об ошибках!
Ну елки-палки. Не ошибки, а предупреждения. И код переписывать никто не просит. И что значит некому? А
undefined symbol
кто выдает? Чем это отличается отunused symbol
?
В объектнике лежит информация о названиях функций, линкер на этапе работы (а как известно, линкер и компилятор — это уже давно один и тот же исполняемый файл, нет уже между этими словами такой границы, какая была ранее) смотрит, кому что нужно. Если функция никому не понадобилась — она не используется (разумеется, компилятор должен оставить где-то (например, внутри объектника) информацию о том, используется ли функция внутри него самого или нет. Если сейчас не оставляет, значит должен оставлять. Ни за что не поверю, что формат .o файлов нерасширяемый и его ни разу не приходилось расширять. В любом случае, это решаемая техническая проблема).
khim
29.09.2017 00:25+4Противоречие в том, что странные преобразования, приводящие к неочевидным багам, компилятор делает для оптимизации, а рядом вы забиваете на нее большой болт и пытаетесь в рантайме выбрать из одного варианта. Зачем?
Не «зачем», а «почему». Потому что мне так удобнее. Если код нужно будет исполнять на системах, где возможны несколько несовместимых сопроцессоров (да хотя бы выбирать между SSE4.2 и AVX'ом), то это может быть полезно переключать в рантайме. А если мы тот же код компилируем под одну платформу (скажем прошивка под конкретную железку) — то компилятор всё заинлайнит и все довольны.
Почему в бинарнике для этой платформы нельзя выкинуть неиспользуемый код для других платформ? Зачем он там нужен?
В бинарнике, как мы убедились, его и нет. Он есть в исходниках — но это уже совсем другая история.
Да почему в процессе компиляции файла
Потому что там и только там используется компилятор C/C++.module.cc
-то???
Я вам уже сколько сообщений подряд твержу, даже в скобках детально пояснять все стал. Не в процессе компиляции, а в процессе линковки всех объектников программы!
В процессе линковки всех объектников компилятор не участвует. Этим занимается совсем другая программа — линкер. О C++ она знает чуть более, чем ничего.
Как код вашего умного Пети будет работать, если компилятор даже понятия не имеет, вызывается ли функция
В соответствии со стандартом, очевидно.NeverCalled
или нет!???
А
Тем, чтоundefined symbol
кто выдает? Чем это отличается отunused symbol
?undefined symbol
— это ошибка, аunused symbol
— нормальная ситуация. В типичной программе — сотни, тысячи, десятки тысяч unused symbol'ов! Откуда они берутся? Да очень просто. Простейший пример:
Прличный компилятор почти наверняка вставитint square(int x) { return x * x; } int cube(int x) { return square(x) * x; }
square
вcube
— но удалить её он не может, так как её кто-нибудь может «снаружи» позвать. Или даже любой класс: ABI C++ так устроен, что каждый конструктор должен присутстьвовать в двух ипостасях — «финальный» и «нефинальный». Если у класса нет потомков — то «нефинальный» конструктор никто вызывать не будет. И ещё десятки других вариантов.
Линкер с опцией --gc-sections всё это безобразие может убрать, но выдавать предупреждения по этому поводу — это бред.
а как известно, линкер и компилятор — это уже давно один и тот же исполняемый файл, нет уже между этими словами такой границы, какая была ранее
Чего? Вы с ума сошли или где? Линкеры — живут тут и тут, компиляторы — тут и тут. Мужду ними не просто «границы» — это вообще разные проекты, выпускаемые разными людьми в разные моменты времени.
Если вы даже этого не понимаете — то о чём вы тут вообще говорите?
Ни за что не поверю, что формат .o файлов нерасширяемый и его ни разу не приходилось расширять.
Да проблема не в .o файлах! Во-первых расширять их действительно непросто, так как компилятор, линкер, отладчик и прочее — это всё разные проекты. А во-вторых — нет смысла ругаться на нормальную, штатную ситуацию.
Если у вас будет выдано 10'000 сообщений из-за того, что всевозможные за'inline'ные функции не используются — то никому от этого хорошо не будет. А чтобы грамотную диагностику сделать с учётом наличия многих компиляторов и многих линкеров, а также инлайнинга и автоматически создаваемых функций (всякиеtypeinfo
) — это нужно не один человеко-год в это вбухать. И ради чего? Чтобы выдать сообщение, которое человек, считающий, что обращение кnullptr
обязательно порождает GPF даже читать не будет?
Проблема со всеми этими предпреждениями в том, что тем людям, которые могут понять «о чём это» — они, в общем и целом, не нужны, а те, кому они, как бы, могут помочь — обычно просто их игнорируют.
Но если вам не жаль угробить пару лет своей жизни — дерзайте, может быть что-нибудь и получится из вашей затеи.
- вы специально удаляли код ее вызова из вашего бинаря (помним, мы рассматриваем ситуацию, когда вызова
Kobalt_x
28.09.2017 06:02Знает он только если всё собирается с lto если собираются отдельные модули то ничего неизвестно
Mingun
28.09.2017 20:35Это почему это? В каждом собираемом объектнике наружу торчат "порты", в которые нужно подключить "порты" других объектников, что и делается на этапе линковки. Если какие-то порты оказались незаполненными — это ошибка линковки —
undefined symbol
называется. Вот если мы заюзалиNeverCalled
илиInitEngine
в одном из таких портов, это автоматически означает, что они используются, а если нет — автоматически, что не используются.khim
28.09.2017 21:21+1Вот если мы заюзали NeverCalled или InitEngine в одном из таких портов, это автоматически означает, что они используются, а если нет — автоматически, что не используются.
Серьёзно? Берём модуль из статьи, добавляем следующее (в другом файле):
И вуаля: согласно вашему критерию — мы «заюзали»void NeverCalled(); void SomeoneIsAnIdiotBwahaha() { NeverCalled(); }
NeverCalled
, все «порты» «запортили» и вообще всё стало ништяк. Вот только неопределённое поведение никуда не делось…Mingun
28.09.2017 21:53-2Функция используется другой функцией? Используется. То, что это вторая функция не вызывается — уже другая проблема. Предполагаем, что компилятор ее не выкинул (если выкинул, как мертвый код — возвращаемся к ситуации, когда ее совсем не было, с которой уже разобрались). Хотя и это проблема решаема, нужно только знать, кто что вызывает. Компилятор граф вызовов строить может.
Опять же, вы почему-то считаете, что любое решение с дополнительными предупреждениями должно быть панацеей и уж если предупреждать, то непременно обо всем и ни разу не ошибаться. Тогда как я говорю о выявлении хотя бы очевидных случаев, как в статье. А усложнить поведение и начать выявлять все большее количество случаев можно будет по мере развития.
khim
29.09.2017 00:36+1То, что это вторая функция не вызывается — уже другая проблема.
Нет — это та же самая проблема. Вы жалуетесь, что компилятор порождает код, который предполагает что определённая функция будет вызвана — но вы не можете определелить — будет ли она вызвана на самом деле! В конце-концов там может меню быть и инструкция для оператора «перед тем, как запускать вычисления зайдите в меня „выбор FPU“ и укажите правильный сопроцессор, в противном случае программа может не работать». И как вы это всё будете отслеживать в вашем линкере?
Компилятор граф вызовов строить может.
Он не может даже понять — вызовется ли та или иная функция или нет. проблема остановки — и это если ещё оператор не задействован! В противном случае вопрос провоцирования UB и вовсе нерешаем…
Тогда как я говорю о выявлении хотя бы очевидных случаев, как в статье.
«Неочевидный случай как в статье» встречается дай бог к 0.01% случаев использования подобной оптимизации. Если вам охота тратить время и силы на решение этой, высосанной из пальца, в общем-то, проблемы — дерзайте. Но у разработчиков компиляторов другие, более насущные, проблемы есть, чем гоняться за редкими багами в криво написанных программах с ошибками.
А усложнить поведение и начать выявлять все большее количество случаев можно будет по мере развития.
Осталось понять кто за всё это будет платить. Какую практическую проблему мы решаем? Как часто люди пишут подобный код и как часто у них возникают эти проблемы? Судя по тому, что пример был опубликован три года назад и до сих пор это никого не волновало — проблема эта редкая и мало кого напрягающая. Вбухивать в её решение несколько человеко-лет — смысла никото не видит… Ну из тех, кто реально что-то может сделать, я имею в виду…
tyomitch
01.10.2017 00:41+1Функция используется другой функцией? Используется. То, что это вторая функция не вызывается — уже другая проблема. Предполагаем, что компилятор ее не выкинул (если выкинул, как мертвый код — возвращаемся к ситуации, когда ее совсем не было, с которой уже разобрались). Хотя и это проблема решаема, нужно только знать, кто что вызывает. Компилятор граф вызовов строить может.
Напомню, вопрос не в том, вызоветсяNeverCalled()
или нет — вопрос в том, вызовется ли она до вызоваDo()
.
Удаление мёртвого кода тут вообще никаким боком.
slonopotamus
27.09.2017 23:09+2Я внимательно читал статью, но плохо прочитал комментарий на который ответил, извините.
С UB дело такое. Да, когда он случается, компилятор (и рантайм) имеют право делать что угодно. Но во всём многообразии этого «что угодно» есть более лучшие варианты поведения чем другие. Было бы хорошо если бы у компилятора был флажок «не делай UB-elimination». Потому что, как вы правильно говорите, переменная может быть либо nullptr либо EraseAll. И компилятор не способен доказать что она никогда и ни за что не будет nullptr. Таким образом мы избавляемся от UB в compile-time (наш код компилируется ожидаемым, предсказуемым образом) и переносим его в run-time. А уже в run-time для нашей конкретной платформы вызов функции по нулевому указателю имеет вполне конкретное поведение — шлёпнуться по SIGSEGV. В момент компиляции компилятор уже вполне в курсе target-платформы и уже имеет право знать как на ней обрабатываются вызовы функций по nullptr.
С точки зрения спецификации, ничего не изменилось — как был UB, так UB и остался (ибо спецификация ничего не говорит об SIGSEGV). Зато фактическое поведение перестало вызывать WTF и диагностируется вполне штатными механизмами. В общем, principle of least astonishment рулез.khim
27.09.2017 23:45+2С точки зрения спецификации, ничего не изменилось — как был UB, так UB и остался (ибо спецификация ничего не говорит об SIGSEGV). Зато фактическое поведение перестало вызывать WTF и диагностируется вполне штатными механизмами. В общем, principle of least astonishment рулез.
А вот принципы «10x тормоза» — это не рулез ни разу.
Описанная тут оптимизация позволяте заинлайнить функции, что позволяет проделать кучу других оптимизаций. Примеры кода, где это даёт заметное примущество — вполне не академические.
lorc
28.09.2017 01:49+1В момент компиляции компилятор уже вполне в курсе target-платформы и уже имеет право знать как на ней обрабатываются вызовы функций по nullptr.
Извините, но нет. В момент компиляции ничего не знает про рантайм и настройки процессора. Я вполне могу замапить по нулевому адресу страничку памяти и у меня не будет никаких SIGSEGV. Мой рантайм (например bare metal прошивка) может ничего не знать про SIGSEGV.
Вы не забывайте что C — это не только x86. И что его рантайм — это не только «богатая» ОС.
laphroaig
27.09.2017 17:15+10Вот из-за таких вот сюрпризов у меня «непонятно почему правильно работающая программа» вызывает на порядок больше паники, чем «непонятно почему неправильно работающая программа»
Resert
27.09.2017 17:20+5Ждём когда юный Джуниор соберет это и запустит на проде от рута!
bfDeveloper
27.09.2017 17:38Кстати да, очень жёсткий пример для публичного кода. Можно было ограничиться prinf(«FAIL»); Я вот сразу пошёл компилить и проверять. Хорошо хоть clang'ом скомпиленное не запускал. Из-под рута не сижу, но приятного мало и для юзера.
AllexIn
27.09.2017 18:07+1А чем выполнение под рутом лучше выполнения под юзером?
Насколько я понимаю — снесется всё до чего дотянется rm, в том числе и home/Мне вот ОС не ценна. Переставлю за час.
А вот потеря пользовательских данных — уже более критична. Хоть большая часть и бэкапится — разгребать последствия придется всё равно…
Vadem
27.09.2017 18:23+2Вроде как сейчас в большинстве дистрибутивов эта команда не выполнится.
Нужно писать rm -rf --no-preserve-root /
Проверять это я конечно не буду :)
michael_vostrikov
27.09.2017 17:47При этом компилятор неявно предполагает, что функция NeverCalled может быть вызвана из неизвестного при компиляции данного файла места (например, глобального конструктора в другом файле, который, возможно, сработает до вызова main)
А почему он не предполагает, что там же может присваиваться другое значение переменной
Do
?technic93
27.09.2017 17:57+8Do объявлен static
michael_vostrikov
27.09.2017 20:30А, ну да, в статье же написано. По незнанию не полностью понял смысл той части)
DCNick3
27.09.2017 21:34+3В С ключевое слово static гарантирует использование переменной/функции только в том модуле, в котором она объялена (она не экспортируется).
homm
27.09.2017 18:14+4Но мы должны признавать, что с того момента, как мы допустили в коде своей программы неопределённое поведение, оно реально может быть насколько угодно неопределённым.
Вот тут явная логическая ошибка. Никто не «допускает» в своей программе неопределённое поведение. Программисты пишут код, программист никогда не пишет «неопределенное поведение начинается здесь». В этом коде может быть, а может не быть неопределенного поведения. Код, содержащий неопределенное поведение ничем не отличается от кода, не содержащего его.
Кто-то конечно может считать, что «настоящий программист» должен помнить наизусть всю спецификацию языка, с которым работает. Проблема в том, что эти 15 человек на планете (попадающие под определение «настоящего программиста») физически не смогут написать весь код, который необходим всему остальному человечеству.
Конечно, игра «напиши еще одну строчку кода и не отстрели себе голову» — очень увлекательная логическая головоломка. Но я хоть убей не понимаю, как ей пользоваться для написания настоящих приложений в 2017 году.
ozkriff
27.09.2017 18:16+2Эм. Я уверен это было в значении "допустил ошибку", только "ошибка" конкретная — UB.
MacIn
27.09.2017 18:18+1О том и речь:
Конечно, игра «напиши еще одну строчку кода и не отстрели себе голову» — очень увлекательная логическая головоломка. Но я хоть убей не понимаю, как ей пользоваться для написания настоящих приложений в 2017 году.
ozkriff
27.09.2017 18:32А, ну если это было не в смысле "компилятор виноват", а "язык язык", то я целиком согласен и выход простой — использовать другие языки, где меньше UB. Или высокоуровневые (когда задача позволяет), или более хитрые низкоуровневые ;) .
khim
27.09.2017 18:35+4Код, содержащий неопределенное поведение ничем не отличается от кода с ошибкой, не содержащего его.
Так правильнее будет, не находите?
Компилятор не заметит и не исправит ошибку на единицу, не заменит неправильное "/2" на "*2" (из-за которого, скажем, будет просматриваться только четверть массива вместо полного) и так далее.
Почему вдруг компилятор должен вот конкретно вот эти вот ошибки обратывать и «обслуживать» особо? Программист совершил ошибку — он же её и исправит (после восстановления данных из backup'а).MacIn
27.09.2017 18:38-3Почему вдруг компилятор должен вот конкретно вот эти вот ошибки обратывать и «обслуживать» особо? Программист совершил ошибку — он же её и исправит (после восстановления данных из backup'а).
Потому что здесь нет ошибки программиста.ozkriff
27.09.2017 18:48+4А чья же? Программист взялся писать код на языке, но не смог соблюсти все инварианты.
Можно сказать что отчасти это ошибка авторов языка, отчасти может быть ошибка разработчиков компилятора, отчасти ошибка выбиравшего язык для проекта, отчасти может быть ошибка настройщика статического анализатора и всякой такой фигни… Но привел-то все это в действие кривым кодом именно программист, так что он явно хотя бы отчасти виноват.
khim
27.09.2017 18:54+3Потому что здесь нет ошибки программиста.
Серьёзно? А тут:
Тоже всё зашибись? А тут:int *p = new int[100]; for (int i=0; i<1000000000; i++) p[i]=i;
Тоже всё распрекрасно? Или вот так:int *p = new int[1000000]; delete[] p; for (int i=0; i<1000000000; i++) p[i]=i;
int* foo() { int arr[100]; for (int i=0;i<100;i++) { arr[i] = i; } return arr; }
Нет, не ндравится? Не хотите заставить компилятор добиваться, чтобы такие программы работали? А в чём отличия с примером из статьи?michael_vostrikov
27.09.2017 21:02+1Отличие в том, что в примере из статьи программистом не указан вызов функции, а в ваших примерах указано обращение к массиву.
khim
27.09.2017 21:40+2Ну то есть вы всё-таки предлагаем делить UB на «хорошие» и «плохие». Ok.
Что будем с подобным примером делать? Оптимизировать или мириться с тем, что какой-нибудь ICC будет наш компилятор рвать «как тузик грелку» в некоторых тестах и некоторых программах?michael_vostrikov
28.09.2017 09:17-2Нет, это было к вопросу о том, почему в статье нет ошибки программиста, а у вас есть. Там не написано, а компилятор предположил, а тут написано явно. И да, это разные виды UB, и вполне можно обрабатывать их по-разному.
Как решать я уже написал. Нужно явно обозначать, что функция вызывается извне. Тогда в статье обозначения не будет (и будет честный вызов NULL), а в примере с движком будет (и будет оптимизация).mayorovp
28.09.2017 09:25+1А чем вам не устраивает подход "явно обозначать, что функция не вызывается извне"?
michael_vostrikov
28.09.2017 09:35Тем, что он приводит к ситуации из статьи. Раз компилятор не видит всю программу целиком, значит надо сообщать ему о поведении других частей, а не разрешать ему додумывать. Тем более что проблема больше связана с инициализацией, и модули бы решили эту проблему. Вызывается в конструкторе (который вызывается извне)? Значит такая оптимизация корректна.
mayorovp
28.09.2017 09:40+2В данный момент существует способ сообщить компилятору информацию о поведении других частей.
Более того, если и правда сообщить компилятору что NeverCalled снаружи тоже не вызывается — эффект пропадает
AllexIn
27.09.2017 18:57Вызов метода по заведомо нулевому указателю — не ошибка??
Нет, я, конечно, последователь метода failfast, но даже он не предполагает, что мы будем ронять приложение заведомо кривыми вызовами…homm
27.09.2017 19:00Вызов метода по заведомо нулевому указателю — не ошибка??
А вдруг нет, если именно такой была цель — вызвать исключение доступа?
khim
27.09.2017 19:13+3А вдруг нет, если именно такой была цель — вызвать исключение доступа?
То у вас случилсяepic fail
, если вы исполняете программу на ARM без MMU. Там по адресу 0 живёт процедура инициализации, которая перезапускает всю систему. Не совсемrm -rf
— но почти. И без всякой «помощи» со стороны компилятора…homm
27.09.2017 19:16Ну вот видите, вы даже более полезное применение нашли такому поведению. В чем же тут тогда epic fail?
khim
27.09.2017 19:27+5В чем же тут тогда epic fail?
В том, что ожидаемого поведения (вызвать исключение доступа) вам получить не удалось.
А стало быть в переносимой программе его быть не должно. Я уже писал.
Принцип такой: если ваша программа не будет работать на процессорах Cray (где, внезапно,nullptr
не состоит из одном нулевых битов), если она не будет ботать на системе с дополнением до одного, если она не сможет работать на процессоре без MMU (где вызов кода по адресуnullptr
«уничтожает» вашу программу) и т.д. и т.п. — то эта программа ошибочна.
Разработчиков компилятора не волнуют ваши отговорки типа «ну мой же код будет работать тольго под Windows и только под 32-бит!» — если вы хотите писать только и исключительно под Windows и только под 32-бит, то вам нужно выбрать язык и компилятор, которые ничего другого не собираются поддерживать никогда.salas
27.09.2017 20:56+1если вы хотите писать только и исключительно под Windows и только под 32-бит, то вам нужно выбрать язык и компилятор, которые ничего другого не собираются поддерживать никогда.
А допустим, вот прямо так. Только Windows и только 32 бита, желаемый уровень абстракции — ну, в общем, такой, на котором можно вызывать WinAPI без обёрток. Какие языки и компиляторы Вы бы посоветовали?
ozkriff
27.09.2017 20:58+3Это ужасно, конечно, но тогда уж можно полностью завязываться на поведение конкретной версии VS, наверное, и не париться "о высоком" и о том как будет житься поддерживающим впоследствии этот код людям.
salas
27.09.2017 22:03Я в целом догадываюсь, что именно такова суровая исторически сложившаяся реальность за пределами тёплой и сухой области применимости условных питона или джавы — или Cray без MMU, или конкретные версии компиляторов, и никаких компромиссов. Но, может быть, что-то пропускаю? Процитированная рекомендация подразумевает что-то третье ("никогда" — это, кажется, не про конкретную версию).
slonopotamus
27.09.2017 23:17Пока мы говорим про программу в вакууме — да, вы правы. Но как только в дело вступает компилятор, у нас появляется знание о целевой платформе и её свойствах. Поэтому на этапе компиляции часть UB превращается во вполне себе defined behavior.
khim
27.09.2017 23:52+3Поэтому на этапе компиляции часть UB превращается во вполне себе defined behavior.
Только в том случае если вы это запросили явно. Хотите чтобы переполнение у целых числе не считалось UB? Задайте -fwrapv. Хотите использовать каламбур типизации? -fno-strict-aliasing — и нет проблем.
Но по умолчанию — нет. Программа должна работать на CRAY MP с дополнением до одного — и точка.
homm
27.09.2017 18:57+6Так правильнее будет, не находите?
Не нахожу. В Си, насколько я понимаю, даже порядок вычисления аргументов не определен. Вы приравниваете вызов любой функции с вычисляемыми аргументами к ошибке?
не заменит неправильное "/2" на "*2"
Не понимаю, что вы пытаетесь сказать. То, что компилятор (стандарт языка) не может магическим образом исправить неправильный код, по-вашему каким-то образом дает ему карт-бланш ломать правильный код?
khim
27.09.2017 19:19+1В Си, насколько я понимаю, даже порядок вычисления аргументов не определен.
Не полностью определён. Но описаний того, что произойдет достаточно для того, чтобы писать программы.
Вы приравниваете вызов любой функции с вычисляемыми аргументами к ошибке?
Если там, скажем, два разаi++
— то да. это ошибка. Если ничего подобноно нету — то нет, это нормальный код. В спеке описано что про него можно сказать, чего сказать нельзя.
То, что компилятор (стандарт языка) не может магическим образом исправить неправильный код, по-вашему каким-то образом дает ему карт-бланш ломать правильный код?
Любой код, вызывающий UB — неправильный. По определению. Как я уже писал программа, вызвающая UB может исполнять что угодно и когда угодно — причём в стандарте отдельно подчёркнуто, что «когда угодно» — распространяется, в том числе, на время до точки, когда UB произойдёт.homm
27.09.2017 19:23-6Любой код, вызывающий UB — неправильный.
Еще раз — почти любой код на Си содержит UB, как минимум из-за прядка вычисления аргументов. Получается «Почти любой код, написанный на Си — неправильный». Так зачем пользоваться таим языком?
khim
27.09.2017 19:35+9Еще раз — почти любой код на Си содержит UB, как минимум из-за прядка вычисления аргументов.
Порядок вычисления аргументов — это UNSPECIFIED behavior, ни в коем разе не undefined.
Так зачем пользоваться таим языком?
Человеку, который путается в терминах и не понимает, что unspecified, undefined и implementation defined — это всё разные вещи с разными последствиями действительно стоит использовать какой-нибудь другой язык, попроще.
Vindicar
27.09.2017 19:16+1Я, конечно, не профи, но все же…
По-моему тут нарушается принцип наименьшего удивления.
Падение программы в GPF — меньший (и менее неприятный) сюрприз по сравнению с вызовом не той функции. Проще отловить, проще понять, проще исправить.khim
27.09.2017 20:40+3Эта программа — ошибочна. Проблемы возникают только когда вы вызываете UB. Точка. Конец дискуссии. Как я писал выше — эту программу таки можно использовать так, чтобы не вызывать UB. И вот в этом случае — она будет работать корректно.
А вот как она будет работать если вы вызываете UB — разработчиков компилятора не волнует. Как-то работает — и ладно.
potan
27.09.2017 20:45Указатель на функцию можно не только вызвать, но и сравнить с другим указателем или с null. Так что такое преобразование некорректно, если компилятор не проверил отсутствие таких сравнений.
khim
27.09.2017 21:08+1Указатель — локальный для модуля, так что компилятор именно что проделал подобные исследования и потому смог удалить код из
NeverCalled
.
Добавьте функцию, которая позволить вам на эту переменную посмотреть из другого модуля — и функция NeverCalled станет непустой и переменнаDo
вернётся. Но изmain
по прежнему будет вызыватьсяrm -rf
, так как ничего другого без попадания на UB вы сделать не сможете.
Zakyann
27.09.2017 20:47-6Как это по-хабовски — минусовать за собственное мнение. Смешно смотреть, как 90% боятся высказаться против 'тренда' из-за боязни слить карму. Но мне пофиг, я не кармадрочер. Аккаунт сгниёт — еще один заведу :) Минусуйте на здоровье :)
Зачем мне думать за компилятор? Всё, что максимально можно сгрузить на компьютер, должно быть на него сгружено.
Есть возможность максимально проверить типы? Делаем. Есть возможность однозначно выполнять код — делаем. Есть возможность максимально уйти от ошибок — делаем.
Падение программы в GPF — меньший (и менее неприятный) сюрприз по сравнению с вызовом не той функции
Это, конечно, вообще ад. В делфи-паскале такое представить сложно. Разве что программер сам накосячит и указатель не на ту функцию передаст. Но это уже его косяк, а не 'особенности' реализации.0xd34df00d
27.09.2017 21:29+2Есть возможность максимально проверить типы? Делаем.
Просто странно приводить паскаль в пример такого языка.
Zakyann
27.09.2017 21:56А что с ним не так? Есть какие-то проблемы с проверкой типов?
0xd34df00d
27.09.2017 22:08+2Далеко не самый строгий язык с далеко не самой выразительной системой типов.
Zakyann
27.09.2017 22:33Лучше всего, когда слова подкрепляются примерами.
0xd34df00d
27.09.2017 22:34+2Rust, Haskell, Clean, Idris. Да мало ли.
Zakyann
28.09.2017 04:56-1Знаете, сколько я еще интересных названий языков знаю? :) Сравнения какие-то будут? Покажете более выразительную систему типов или более строгую типизацию?
0xd34df00d
28.09.2017 06:23+2Взгляните на таковые в этих языках, например.
Zakyann
28.09.2017 17:21-4Понятно. Вопросов больше не имею :)
0xd34df00d
28.09.2017 20:56+2Я не понимаю, какого ответа вы ожидали. Сравнения по пунктам различных систем типов? Рассказа, что в паскале можно из того, что в тех языках нельзя?
splav_asv
27.09.2017 21:57+1Вашим воззрениям на язык, язык C просто не удовлетворяет. Чем уверять, что язык плохой, просто пользуйтесь другими. У языка C и языка C++ пока ещё есть уникальные свойства, благодаря которым во многих областях их заменить ничем особо не получается.
Со временем, конечно, языки постепенно стремятся к вашему представлению, но это отнюдь не просто.
mayorovp
28.09.2017 09:10У вас есть какие-то пруфы относительно того что 90% боятся высказывать свое мнение?
RolexStrider
27.09.2017 21:51typedef int (*Function)();
Уже одна эта строчка сразу навела на мысль, что дальше по тексту будет про UB и прочая чёрная магия. Так и оказалось.johnnymmc
27.09.2017 21:58-1Интересно, кто-то в здравом уме вообще может написать такое в реальном, не «эзотерическом» проекте?
tangro Автор
28.09.2017 10:44+7Какое «такое»? Указатель на функцию тайпдефнуть? В каждом первом реальном проекте на С/С++ такое есть.
DmitryMe
28.09.2017 11:23+1Практический пример — вызов функций WinAPI, доступных только в части версий Windows, на которых должна работать программа. Указатель на функцию получается вызовом GetProcAddress(), его нужно потом привести к типу «указатель на фнукцию с такими-то параметрами и таким-то возвращаемым значением».
ozkriff
27.09.2017 22:11+4Вроде ж просто сишный псевдоним для указателя на функцию. Я наоборот видел мало кода, где с указтелями на функции работают без typedef (спасибо "уникальному" синтаксису объявлений си).
Или дело просто в том что есть указатель на функцию?
johnnymmc
27.09.2017 21:56А не логичнее ли компилятору (или вообще препроцессору) вообще выкинуть неиспользуемую функцию?
khim
27.09.2017 21:58+4А как он узнает, что она не используется? Её могут из другого модуля вызывать.
DistortNeo
27.09.2017 22:01Не могут. Она же
static
.khim
27.09.2017 22:10+1Так все
static
от извёл. ОнNeverCalled
извести не может — но может сделать пустой. Что и было проделано.
MrShoor
28.09.2017 01:29Проанализировать исходник аж всего приложения, и увидеть, что в Do значение присваивается лишь однажды — у компилятора мозгов хватило. А элементарно то, что функция, в которой есть это самое присвоение никогда не вызывается — нет? Что за странный оптимизатор?
lorc
28.09.2017 01:58+4А элементарно то, что функция, в которой есть это самое присвоение никогда не вызывается — нет?
Она может вызваться из другой единицы трансляции. Компилятор — не линковщик, всю программу целиком не видит.MrShoor
28.09.2017 04:16Да, действительно, спасибо. Ведь Do объявлена как static, и поэтому достаточно проанализировать только этот файл. А я ошибочно подумал, что компилятор проанализировал всю программу.
Nick_Shl
28.09.2017 04:28Добавить к "static Function Do;" volatile и 99% даю, что будет вызываться NULL.
gul_kiev
28.09.2017 10:43+2Забавно, что это перекликается с математическим внутренне противоречивым высказыванием. Если из него делать выводы, предполагая, что оно не противоречиво, то можно прийти к чему угодно — из того, что дважды два пять, можно сделать вывод, что у собак есть крылья. Именно так и делает компилятор: предполагает, что в коде UB отсутствует, и получает произвольные удивительные результаты. Понятно, что такое предположение может иногда слегка ускорить правильные программы, но точно ли такая жертва неправильными оправдана? Почему бы не допускать, что программист мог ошибиться, и в программе есть UB?
Насколько я понимаю, изначально UB появилось из-за невозможности гарантировать результат там, где он зависит от архитектуры и других внешних факторов. Но программист может знать, что его программа будет запускаться только на x86, может даже добавить assert на эту тему. На каких-то платформах деление на 0 не вызовет exception, но если компилятор, исходя из этого, будет делать что угодно с программой, пытающейся разделить на ноль на x86, по-моему он будет неправ. Если программист прибавляет единицу к unsigned int, а потом смотрит, не получился ли 0 — формально он неправ, но ещё больше будет неправ компилятор, если выкинет эту проверку как невозможную, или если скомпилирует код в «rm -rf /», хотя формально компилятор при этом не нарушит стандарт.
На C часто пишутся программы без требования переносимости, на то он и низкоуровневый, поэтому мне бы казалось разумным многие случаи undefined behavior перевести в unspecified behavior или в implementation defined. Например, при integer overflow может получиться любое число или exception, но не что-либо ещё (например, не «rm -rf /») — такое правило мне казалось бы естественным. Передача управления по NULL — sigsegv на тех архитектурах, где это вызывает прерывание, и UB там, где может выполняться мусор.khim
28.09.2017 12:30+5Почему бы не допускать, что программист мог ошибиться, и в программе есть UB?
Потому что о правильных программах мы можем судить — в спеке есть описания того, что они должны делать и как. О неправильных же программах мы судить не можем — мы не знаем чего хотел программист и очень часто программист этого тоже не знает. Попытки же «выудить» из него эту информацию тоже добром не кончаются ибо разные программисты ожидают разного поведения, когда их программа совершает UB.
Но программист может знать, что его программа будет запускаться только на x86, может даже добавить assert на эту тему.
А может также ничего не знать. А ещё может оказаться, что нам нужно будет эмулировать тонкие краевые эффекты. Например 32-битная единица, сдвинутая вправо на 33 бита на x86 даст 2, а на ARM'е — 0. Если у нас кросс-компилятор и мы полагаемся на то, что UB в программе нет, то мы можем сдвигать когда угодно и что угодно, так как программист должен будет обеспечить, чтобы сдвига на 33 в программе не случилось. Если же мы разрешим такие сдвиги, то придётся либо писать эмулятор «сдвигов типа ARM» на x86 и использовать его при рассчёте констант, либо отказаться от их рассчёта.
Если программист прибавляет единицу к unsigned int, а потом смотрит, не получился ли 0 — формально он неправ
Если кunsigned
, то он прав. И формально и фактически.
На C часто пишутся программы без требования переносимости, на то он и низкоуровневый, поэтому мне бы казалось разумным многие случаи undefined behavior перевести в unspecified behavior или в implementation defined.
Позиция разработчиков компиляторов проста и незатейлива: хотим чего-то подобного — пишем пропозал — получаем результат.
Просто потому что ссылки на здравый смысл не работают. Он у всех разный, как показывает этот тред…gul_kiev
28.09.2017 20:16> Если к unsigned, то он прав. И формально и фактически.
О, вот хороший пример. Я знаю, как оно на низком уровне, и знаю, что integer overflow — это UB. Поэтому не удивлюсь, если и для unsigned int переполнение окажется UB. По стандарту не оказалось, фух. Но если для signed переполнение UB — это всё равно минное поле.
netch80
29.09.2017 12:10> Если программист прибавляет единицу к unsigned int, а потом смотрит, не получился ли 0 — формально он неправ
Именно для unsigned правила выглядят так:
C99 пункт 6.2.5.9:
>> A computation involving unsigned operands can never overflow, because a result that cannot be represented by the resulting unsigned integer type is reduced modulo the number that is one greater than the largest value that can be represented by the resulting type.
Аналогичное правило в C++11, пункт 3.9.1.4:
>> Unsigned integers, declared unsigned, shall obey the laws of arithmetic modulo 2n where n is the number of bits in the value representation of that particular size of integer.
А вот для signed они одинаково (косвенно) определяют, что переполнение — UB.
Как результат, можно проверять переполнение (сильно менее эффективно, чем напрямую машинными средствами) через перевод в unsigned и изучение результата; и точно так же можно реализовывать «заворачивающуюся» арифметику.
С другой стороны, я согласен с общей идеей. В результате подобного подхода со стороны стандартизаторов и авторов компиляторов, поворачивающих «закон» в свою сторону, я уже слышал много сообщений типа «ну вас нафиг, ухожу на Java/C#/Go/etc.» именно за счёт гарантий, которые даёт эта группа; часто их даже не интересует managed memory — их задалбывает мир, где любой неосторожный шаг приводит к падению в пропасть.
> поэтому мне бы казалось разумным многие случаи undefined behavior перевести в unspecified behavior или в implementation defined.
+100.
Qualab
28.09.2017 15:21-7Давайте честно скажем, что в шланге багло, несмотря на Undefined behavior, компилятор берёт на себе излишне много.
khim
28.09.2017 16:58+3Давайте скажем честно: это камлание из серии «собака лает — караван идёт». В точности нуль разработчиков придерживаются этой точки зрения. То есть ни одного «сочувствующего» вы не найдёте.
Дело в том, что в данном случае «стреляет» не экзотическая оптимизация, которая «срабатывает» на каком-то синтетическом бенчмарке, а вполне себе реальная, которая позволяет оптимизировать реальные программы. Идею кода, где это бывает я обрисовывал.
Любовь программистов, выросших на Java, устраивать 100500 индирекций делает подобные оптимизации весьма полезными, так что ломать их ради программы с ошибкой — никто не будет.netch80
29.09.2017 12:12> В точности нуль разработчиков придерживаются этой точки зрения. То есть ни одного «сочувствующего» вы не найдёте.
Разработчиков компиляторов, или целевых программ — их пользователей?
Если второе, то в этой дискуссии достаточно примеров, и в реале вокруг меня.khim
29.09.2017 13:08Разработчиков компиляторов
Разработчиков компиляторов или людей, хотя бы способных послать осмысленное предложение по изменению оного компилятора.
или целевых программ — их пользователей?
Как любит говорит Линус: Talk is cheap. Show me the code.
Совершенно непонятно откуда возьмётся компилятор, что-либо делающий по-другому, если его некому разрабатывать. Ну вот совсем некому. Даже хуже — никто из людей, громко тут «бурлящих» не имеет ни малейшего представления о том, что у него «под капотом» и как оно там работает! Вот, Mingun договорился до того, что у него линкер и компилятор стали «одним исполняемым файлом», что явно показывает уровень понимания, согласитесь?ozkriff
29.09.2017 13:18+1Я думаю что netch80 не спорил с неадекватностью этой точки зрения, а как раз говорил что среди прикладных программистов большинство толком не понимают как вообще работает компилятор, так что "ни одного «сочувствующего» вы не найдёте." — не верно, потому что их полно (и это печально).
netch80
30.09.2017 09:39+2И да, и нет. Безусловно, большинство прикладных программистов не понимают, как работает компилятор; но им обычно это и не нужно, нужно иметь положительную часть опыта (как делать) и отрицательную (как не делать, где грабли). Но основное таки не в работе компилятора, а в том, когда он становится усилителем ошибок. Пример в исходном постинге темы не настолько характерен, как, например, этот; см. по тексту, как или изменение опций компиляции, или небольшая правка исходника, не меняющая суть выполняемого, сменяет неограниченный цикл на ограничение 3 итерациями в цикле. Вот это случай, когда возможность сделать UdB откровенно абьюзится авторами компилятора, а программисту найти такое, если кода много и/или оно закопано в макросах, может быть очень тяжело.
Ну а поскольку безошибочных программ вообще не бывает (helloworld?ы не считаем, и то неизвестно, что там в libc) — совершенно очевидно и обоснованно создаётся ощущение минного поля, авторы которого тут же с краю стоят и усмехаются — «ну-тко, кто ещё на что нарвётся?» А с соседнего поля кричат «а у нас мин нет, а ещё есть печеньки (вариант: батарейки)»…tyomitch
01.10.2017 02:00+2Я бы проводил аналогию не с минным полем, а со слесарной мастерской, уставленной мощными станками: если неаккуратно пользоваться, оставит без рук. А из соседней мастерской кричат «а у нас все инструменты надувные, даже не ушибёшься!»
netch80
01.10.2017 07:45В «соседней мастерской» не надувные, а реальные и делающие ровно то, что им задают. А в мастерской C — такие, что если на них хоть чуть-чуть ошибся, они через полчаса (когда ты давно завершил кусок работы и занялся другим) выстрелят, совершенно законно, тебе сверлом в спину.
netch80
29.09.2017 14:03+2> Совершенно непонятно откуда возьмётся компилятор, что-либо делающий по-другому, если его некому разрабатывать. Ну вот совсем некому.
Так есть же кому. Только они в результате уходят и создают своё. Например, читаем спеку на Go:
>> For signed integers, the operations +, -, *, and << may legally overflow and the resulting value exists and is deterministically defined by the signed integer representation, the operation, and its operands. No exception is raised as a result of overflow. A compiler may not optimize code under the assumption that overflow does not occur. For instance, it may not assume that x < x + 1 is always true.
По последнему предложению видно, что это прямой наезд на подходы C/C++. И далее:
>> Shifts behave as if the left operand is shifted n times by 1 for a shift count of n.
и никаких тебе «если >= ширине сдвигаемого, мы включаем джаз». (В отличие от Java, C#, где заворот для знаковых целых определён, а вот правила для сдвигов уже как в C.)
И, что характерно, часть этих людей — из тех же, что когда-то продвигали C. Я не могу понять это иначе, как признание того, что они просто устали от неопределённостей, заданных в группе C, и привлекают массы разработчиков этой определённостью и стабильностью. Да, они хотели бы получить производительность такого же уровня, но реально они предпочитают предсказуемость результата.
А ещё в Java, C#, Go, etc. жёстко определено, что размерность целых — степень двойки, а представление отрицательных — дополнительный код. И это не мешает им работать на >99.99% реально существующих платформ, включая супер-embedded типа SIM-карт.
Зачем предполагать то, что в реальности уже не существует? Вы можете назвать хоть одно реальное железо, где остались бы отрицательные целые в обратном коде (1?s complement)? И почему, с обратной стороны, C завязан на двоичные биты? Почему (извините за провокацию) не рассчитывают на машины, у которых только десятичные цифры, или на троичные, типа «Сетунь»? По-моему, распространённость машин с обратным кодом примерно равна распространённости «Сетуни», то есть нулю.
А даже если там не 0 — то насколько это важно по сравнению с основной массой? Не лучше ли создать профиль, покрывающий практически всех?
> никто из людей, громко тут «бурлящих» не имеет ни малейшего представления о том, что у него «под капотом» и как оно там работает!
Ну я понимаю (не на уровне активного разработчика компилятора, но на достаточном, чтобы знать, что он делает и почему; и собственный опыт на мелкие поделки, включая кодогенерацию). И это не останавливает меня от вопроса, зачем нужен язык, который способен настолько усиливать неизбежные ошибки программиста…
Только не надо, пожалуйста, говорить «кто ниасилил — кыш на другие языки». Это уже и так происходит, к сожалению. Хотелось бы, наоборот, чтобы эту миграцию никто не форсировал.khim
29.09.2017 19:54+1Так есть же кому. Только они в результате уходят и создают своё. Например, читаем спеку на Go:
Какое имеет отношение «спека на Go» к C/C++?
По моему как раз то, что они «уходят и создают своё» — наглядно показывает, в чём проблема. Нельзя создать хороший компилятор, оставаясь в рамках C/C++ и при этом не опираяся на UB — они, в сущности, изначально для это преднозначались, как я уже писал. А вот если вы делаете свой язык — то для вас это не проблема, так как любая реализация языка обязана следовать вашей спеке. И ситуации, когда у вас будет быстрый, но небезопасный компилятор и медленный, но безопасный — у вас не будет. Ибо все компиляторы будут медленными…
и никаких тебе «если >= ширине сдвигаемого, мы включаем джаз».
Как раз-таки наоборот: чтобы поддержать это определение вам нужно в коде, в каждом месте, где вызывается сдвиг иметь маленький кусочек кода, который именно что и будет делать проверки на тему «если >= ширине сдвигаемого, мы включаем джаз».
Просто потому что разные процессоры ведут себя по разному, но ни один (из распространённых) не ведёт себя так, как предписывает спека на Go!
Да, они хотели бы получить производительность такого же уровня, но реально они предпочитают предсказуемость результата.
В этом случае создание своей спеки или даже своего языка — нормальная реакция.
И это не мешает им работать на >99.99% реально существующих платформ, включая супер-embedded типа SIM-карт.
Серьёзно? Да одних роутеров и «умных» лампочек, на которых никакие C#/Java и «не ночевали» больше, чем всех «платформ», на которых они работают! Код на C и даже C++ работает на гораздо, гораздо, ГОРАЗДО большем числе платформ, чем код на C# или Java.
Зачем предполагать то, что в реальности уже не существует?
Это вопрос скорее к комитету по стандартизации. На практике же особо ничего для поддержки подобных платформ не делается. Остался только артефект: беззнаковое переполноение не существует, а знаковое — это UB. Но к этому, в общем, все уже привыкли.
Только не надо, пожалуйста, говорить «кто ниасилил — кыш на другие языки». Это уже и так происходит, к сожалению. Хотелось бы, наоборот, чтобы эту миграцию никто не форсировал.
А её никто и не форсирует. Просто пока есть медицинский факт: компиляторы, которые «строже» наказывают за UB, чем другие — в числе наиболее популярных. Потому что они, ко всему прочему, быстрее и функциональнее. Попытавшись «удержать» пользователей, которые хотят «безопасности и предстказуемости» — есть шанс, что их всё равно не удержат (ибо C/C++ — в любом случае относится к категории сложных и небезопасных языков), но при этом потеряют тех, кому важна скорость.michael_vostrikov
30.09.2017 05:20-3чтобы поддержать это определение вам нужно в коде, в каждом месте, где вызывается сдвиг иметь маленький кусочек кода, который именно что и будет делать проверки на тему "если >= ширине сдвигаемого"
Зачем, если проверки и соответствующее поведение и так находятся в процессоре?
netch80
30.09.2017 09:17+1Как раз сейчас не находятся (в основных командах сдвигов): они игнорируют старшие биты, а для результата «результат сдвига на N бит равен результату N сдвигов на 1 бит» должны не игнорировать.
Кодогенератор Go (он у них свой, доморощенный) реализует это следующим образом: пусть у нас вход:
var a uint32 var s uint32 fmt.Printf("Params? ") fmt.Scanf("%x%d", &a, &s) r1 := a << s r2 := a >> s r3 := uint32(int32(a) >> s) fmt.Printf("%d(%x) %d(%x) %d(%x)\n", r1, r1, r2, r2, r3, r3)
Собственно вычислительная часть выходного кода выглядит так:
дизассемблер с пояснениямиe8 d5 a3 ff ff callq 48d8b0 <fmt.Scanf> 48 8b 44 24 60 mov 0x60(%rsp),%rax ; &a 8b 00 mov (%rax),%eax ; a 48 8b 4c 24 58 mov 0x58(%rsp),%rcx ; &s 8b 09 mov (%rcx),%ecx ; s 89 c2 mov %eax,%edx ; a d3 e0 shl %cl,%eax ; a << s машинный 83 f9 20 cmp $0x20,%ecx 19 db sbb %ebx,%ebx ; (s<32)?-1:0 21 d8 and %ebx,%eax ; r1 = a << s 89 44 24 54 mov %eax,0x54(%rsp) ; for Printf 89 44 24 50 mov %eax,0x50(%rsp) ; for Printf 89 d0 mov %edx,%eax ; a d3 ea shr %cl,%edx ; a >> s машинный 21 da and %ebx,%edx ; r2 = a >> s 89 54 24 4c mov %edx,0x4c(%rsp) ; for Printf 89 54 24 48 mov %edx,0x48(%rsp) ; for Printf f7 d3 not %ebx ; (s>=32)?-1:0 09 d9 or %ebx,%ecx ; (s>=32)?31:s d3 f8 sar %cl,%eax ; signed_a >> s 89 44 24 44 mov %eax,0x44(%rsp) ; for Printf 89 44 24 40 mov %eax,0x40(%rsp) ; for Printf
Код, как для современных процессоров, построен в стиле branch-free. Вычисляется «флаговое значение», которое равно -1 (все единичные биты), если сдвиг в пределах ширины переменной (32), и 0, если сдвиг равен этой ширине или выше; логический AND зануляет результат в случае слишком широкого сдвига. Для сдвига знакового значения вправо ещё хакеристее: так как результаты сдвигов на s>=31 совпадут со сдвигом на 31, это флаговое значение используется, чтобы сдвиг заменить на 31, если он больше, и дальше используется уже машинная команда.khim
30.09.2017 17:35+1они игнорируют старшие биты
Если бы они все хотя бы игнорировали биты — это было бы полбеды. А так — x86 игнорирует все «лишние» биты и сдвиг 32-битного числа на 33 — это то же самое, что сдвиг на 1. А ARM — игнорирует, но не все: сдвиг на 33 — отрабатывается правильно, а вот сдвиг на 257 — это опять то же самое, что и сдвиг на 1.
Собственно поэтому это и UB в C/C++.
А если кому-то нужно гарантировать оптимизированность операции — например, транслятор не может понять, что сдвиг будет в нужных пределах — предоставить какие-нибудь int::native_sll(arg, shiftcount), который будет builtin?ом транслятора.
Если вы делаете свой, новый, язык «с иголочки» — то вы можете себе это позволить. Но если язык у нас уже есть и на нём написаны миллиарды строк кода — то глупо как-то его брать и замедлять на ровном месте…
netch80
30.09.2017 08:58+1> Какое имеет отношение «спека на Go» к C/C++?
Такое, что Go позиционируется, по одному из каналов, как «безопасный C с шахматами и поэтессами». И это реально работает, по тому, что я вижу, в плане миграционных тенденций.
> И ситуации, когда у вас будет быстрый, но небезопасный компилятор и медленный, но безопасный — у вас не будет. Ибо все компиляторы будут медленными…
— Моя программа работает в 4 раза быстрее твоей!
— Зато моя программа работает правильно.
Пусть неправильно она работает по причине незамеченного тонкого ляпа программиста, но для краткосрочного результата это неважно. Если он не сможет быстро это починить, шансы на то, что плюнет и смигрирует на менее проблемный язык, высоки.
> Как раз-таки наоборот: чтобы поддержать это определение вам нужно в коде, в каждом месте, где вызывается сдвиг иметь маленький кусочек кода, который именно что и будет делать проверки на тему «если >= ширине сдвигаемого, мы включаем джаз».
«Джаза» как раз не будет из-за проверки ширины сдвига. Джаз это UdB, который разносит ту программу вщент, включая места, связь которых совершенно неочевидна. А дополнительная реакция достаточно дёшева, а если ещё и есть вычисленная гарантия, что сдвиг будет в пределах ширины, то её удалят.
> Просто потому что разные процессоры ведут себя по разному, но ни один (из распространённых) не ведёт себя так, как предписывает спека на Go!
Ха, ошибаетесь :) Делает, и самый распространённый. Почитайте для x86 доку на семейство PSL{L,R}{W,D}. Если сдвиг шире, чем одиночное значение в векторе, выходное значение обнуляется.
Я не знаю, зачем они это сделали, какой юзкейс стоял над ними. Могу только догадываться, что, как для всего MMX/SSE, они оптимизировали какой-то сверхважный частный алгоритм. Там много непонятного, включая мнемонику — они не стали повторять обычную скалярную x86, а взяли привычную для RISC. И, конечно, чисто формально это никак в данном споре не влияет на общий результат. Но говорить, что «ни один», нельзя.
А ещё Вы говорили рядом:
>> Например 32-битная единица, сдвинутая вправо на 33 бита на x86 даст 2, а на ARM'е — 0.
Проясните? Это тот же случай, или это ARM64 со сдвигами только двойными словами?
> В этом случае создание своей спеки или даже своего языка — нормальная реакция.
Да. Только этого ли хотели добиться, ужесточая наказания за ошибку?
> Да одних роутеров и «умных» лампочек, на которых никакие C#/Java и «не ночевали» больше, чем всех «платформ», на которых они работают!
Потому что никто не видел окупаемой цели в этом переносе. Но не по чисто технической невозможности.
> Остался только артефект: беззнаковое переполноение не существует, а знаковое — это UB. Но к этому, в общем, все уже привыкли.
Так в том и дело, что ой не все. И даже те, кто «в системе» 20+ лет, как я, натыкаются на заботливо расставленные детские грабли (детские — это те, что бьют не в лоб, а в самое чувствительное место). А уж что про новичков говорить. А есть ещё потоки выпустившихся из вузов, где ни один преподаватель не говорит в духе «запомните, здесь водятся супердраконы, съедят — не успеете мяукнуть», зато успешно тренируют алгоритмам и обращению с переменными. И они в значительной доле идут туда, где им никто не расскажет про проблемы, если они не читают хабр или аналогичные умно-заумные ресурсы.
Вот все эти ubsan?ы — начало действительно конструктивного подхода. Начало — потому, что само их наличие до сих пор малоизвестно, а регулярное использование никак не всеобщая практика. В идеале основные жалобы на проблемы должны быть сгенерированы ещё на стадии трансляции.
А если говорить про уровень самого языка — в идеале то, что я хотел бы видеть, это некоторое расширение подхода, как в C# checked/unchecked, только с бо?льшим количеством вариантов. Например, для целых — выбирать раздельно по signed/unsigned исполнение арифметики: wrapping, checked, relaxed (как сейчас signed в C — гарантируется непереполнение), platform native, saturating (последнее — опционально). Тогда, записав
a = [[signed_checked]] (b+c);
мы получаем конкретную желаемую программистом локальную политику, пусть даже это замедлит выполнение.
И, разумеется, всё это на уровне стандартного языка, а не расширениями вроде -fwrapv, и безотносительно общего уровня оптимизации.
Извините за почти бесплодные мечтания (по поводу C).
И спасибо за признание, что UdB для знакового переполнения — это именно рудимент от времён динозавров.
> Просто пока есть медицинский факт: компиляторы, которые «строже» наказывают за UB, чем другие — в числе наиболее популярных. Потому что они, ко всему прочему, быстрее и функциональнее.
Про скорость всупереч корректности я уже сказал выше. Пока что всё это не отменяет того вывода, что сишники своими руками прогоняют тех, кто при относительно небольших усилиях авторов трансляторов мог бы остаться их пользователем. Да, я тут самонадеянно считаю, что описанные выше подходы управляемого уровня агрессивности транслятора реальны и подъёмны. Тот же -fwrapv существует уже много лет, чем эти опции хуже?khim
30.09.2017 18:47+2Такое, что Go позиционируется, по одному из каналов, как «безопасный C с шахматами и поэтессами».
«Позиционироваться» и «являться» — разные вещи.
И это реально работает, по тому, что я вижу, в плане миграционных тенденций.
Вот только есть одно «но»: вы-то это видите, а Роб Пайк — не видит. Вот тут он как раз удивляется этому феномену: он-то делал Go, как «правильный C» — а получил, почему-то, в основном, перебежчиков с Python'а и Ruby. Да что там говорить: в нашем проекте часть вещей была переведена с pyhton'а, но го, но что было на C/C++ — осталось на C/C++.
Пусть неправильно она работает по причине незамеченного тонкого ляпа программиста, но для краткосрочного результата это неважно. Если он не сможет быстро это починить, шансы на то, что плюнет и смигрирует на менее проблемный язык, высоки.
Практика показывает, что нет. Разговоров — много. Реальных миграций — мало. UB реально «бьёт по рукам» — но в основном это разные вещи, которые всё равно бы не работали (типа использования переменной из двух потоков без блокировки), даже в Go.
Почитайте для x86 доку на семейство PSL{L,R}{W,D}. Если сдвиг шире, чем одиночное значение в векторе, выходное значение обнуляется.
Особенно это хорошо работает для байтов, да. И в любом случае кто-нибудь обломается, обнаружив, что сдвиг работает не как VSHL на ARM'е (If the shift value is positive, the operation is a left shift. If the shift value is negative, it is a truncating right shift.)
Стоит ли говорить о том, что в реальных программах сдвиг, почти всегда, хранится в переменной со знаком (обычноint
)?
Но формально — да, вы правы, признаю, был неточен…
А ещё Вы говорили рядом:
Тот же, но не совсем. То есть для чисел до 255 всё работает как в Go. Однако. В ARM2 в barrel-shifter подавался только младший байт операнда. Во всех процессорах (включая самые последние) сдвиг работает так же. Совместимость-с.
>> Например 32-битная единица, сдвинутая вправо на 33 бита на x86 даст 2, а на ARM'е — 0.
Проясните? Это тот же случай, или это ARM64 со сдвигами только двойными словами?
> В этом случае создание своей спеки или даже своего языка — нормальная реакция.
Нет, конечно. Никто никого «ужесточать» не пытается. Пытаются сделать более быстрый компилятор. Достаточно успешно: clang сейчас уже чаще используется, чем GCC.
Да. Только этого ли хотели добиться, ужесточая наказания за ошибку?
Потому что никто не видел окупаемой цели в этом переносе. Но не по чисто технической невозможности.
Технически можно и Windows 10 на Commodore 64 запустить. Вот только не делает никто почему-то.
А если говорить про уровень самого языка — в идеале то, что я хотел бы видеть, это некоторое расширение подхода, как в C# checked/unchecked, только с бо?льшим количеством вариантов. Например, для целых — выбирать раздельно по signed/unsigned исполнение арифметики: wrapping, checked, relaxed (как сейчас signed в C — гарантируется непереполнение), platform native, saturating (последнее — опционально). Тогда, записав
Вот это — уже конструктивный подход. Можно попробовать обсудить его с людьми из Яндекса. Они как раз с конференции вернулись
a = [[signed_checked]] (b+c);
мы получаем конкретную желаемую программистом локальную политику, пусть даже это замедлит выполнение.
И, разумеется, всё это на уровне стандартного языка, а не расширениями вроде -fwrapv, и безотносительно общего уровня оптимизации.
Почему бесплодные? Они бесплодны пока вы тут в комментариях «срётесь». Оформите в виде формального «proposal»а — и у вас есть шанс.
Извините за почти бесплодные мечтания (по поводу C).
Разработчики компиляторов — они не садисты, у них нет цели кого-либо наказать. Просто когда в 99.999% случаев всё и так работает без всяких «лишних» проверок (как в примере со сдвигами выше), то глупо на них тратить время — особенно если стандарт этого не требует.
И спасибо за признание, что UdB для знакового переполнения — это именно рудимент от времён динозавров.
Ну дык никто и не спорит. Знаковое переполнение — это рудимент от старых архитектур, правила алиасинга — это от времён, когда сопроцессор, работающий с правучкой — был физически отдельным процессором. И так далее.
Но проблема в том, что даже если сейчас процессоры стали работать по-другому — это уже ничего не меняет: существующие программы на UB не полагаются (во всяком случае не должны полагаться) — так почему бы это не использовать для оптимизации?
Пока что всё это не отменяет того вывода, что сишники своими руками прогоняют тех, кто при относительно небольших усилиях авторов трансляторов мог бы остаться их пользователем.
Я вот в этом совсем не уверен. Ибо они гонял-гонят, а прогнать никак не могут. На TIOBE C и C++ — по прежнему языки #2/#3, а Go и D — где-то во втором-третьем десятке. Правда относительная популярность падает — но она и у Java падает так же стремительно, так что сдаётся мне, что не в UB дело.
Тот же -fwrapv существует уже много лет, чем эти опции хуже?
Если они всего лишь «не хуже», то смысла в них особого нет. Сейчас проверил: в исходниках Android'а -fwrapv используется в 5 проектах: syslinux, dEQP, Python и mksh. Ни один из них не является критичным и при необходимости от них от всех можно отказаться.
Стоит ли огород городить ради опций, которые всё равно никто не будет использовать?
tyomitch
01.10.2017 01:38+1Совершенно непонятно откуда возьмётся компилятор, что-либо делающий по-другому, если его некому разрабатывать. Ну вот совсем некому.
Переформулирую то же самое с другого угла зрения: компиляторы разрабатываются вполне прагматичными людьми для вполне практических целей. Тот, кто добавлял в Clang оптимизацию, ставшую темой поста — или любую другую оптимизацию — уж точно не потирал ладони: «уж теперь-то вы у меня попляшете, жалкие людишки, допускающие в своём коде UB!» Никто не станет реализовывать абсурдные трансформации кода лишь затем, чтобы досадить программистам — независимо от того, допускает стандарт такие трансформации или нет.
Наоборот: если какая-то оптимизация была реализована и продолжает поддерживаться, это значит, что в реально компилируемом коде она полезна, даже если в отдельных «лабораторных» примерах она кажется абсурдной.
Zakyann
За что я, среди прочего, не долюбливаю плюсы, предпочитая старый-добрый, тепло-ламповый паскаль :)
lorc
В посте приводится пример из чистого C.
Undefined behavior — как раз то, что позволяет компилировать сишный код в эффективный машинный код.
ozkriff
Есть мнение что это уже "слегка" устаревшая точка зрения и с современными знаниями о разработке ЯП и технологиями оптимизации вполне можно иметь системный язык практически без UB. См., например, Rust, где практически все UB возможно только в unsafe блоках.
bfDeveloper
Ценой усложнения языка. Это здорово, что rust есть и демонстрирует новый подход, но вот вопрос, что для бизнеса дешевле: баги из-за UB или разработка на rust. Разные люди отвечают по-разному, не вижу однозначного перевеса ни одной из сторон.
BlessMaster
Сам язык с пользовательской точки зрения — вряд ли сложнее.
А сравнивать сложность "договориться со строгим компилятором" и "договориться с отладикой, тестированием и фазой луны" — достаточно тяжело сравнивать.
ozkriff
По мне так для бизнеса тут скорее важна просто зрелость языков — инфраструктура, библиотеки, количество специалистов и т.п.
WFrag
Усложнение?? Да меня кошмары мучают после того, как я узнал про std::launder!
Хотя, конечно, Rust-овские lifetime тоже не ягодки. В плане использования ещё не сильно сложно, а вот в плане правильного проектирования — сложно.
MacIn
Так или иначе, рациональное зерно в словах Zakyann есть. Оптимизация — это хорошо, кто же спорит. Но если мы приходим к ситуации, когда итоговый машинный код абсолютно не соответствует исходному (спекулятивная трансформация), это ненормально. Разрабатывая программу, ты должен быть уверен, что машинный код будет делать то же самое (я не о процессе, а о результате — процесс может быть иным из-за оптимизации, как, например, сдвиг вместо умножения), что исходный код на ЯВУ. Ты не должен думать за компилятор.
lorc
Возможно данный конкретный пример — это уже перебор. Возможно в данном случае компилятор должен был бы сгенерить вызов функции по адресу NULL. Это более ожидаемое поведение с точки зрения здравого смысла. Но понимаете, очень тяжело засунуть здравый смысл в компилятор.
А с точки зрения стандарта этот UB ничем не отличается от других UB.
MacIn
Не согласен. «Здравый смысл» в рамках компилятора — это сделать отображение ЯВУ на машкод. Так, чтобы получить тот же результат. Если ЯВУ предписывает упасть в General Protection Fault — значит, и машкод должен перейти по адресу 0 и упасть в GPF, а не стереть что-то там, чего исходных текст на ЯВУ не предписывал.
Абсолютно то же самое с UB ситуациями, когда разыменовывается указатель без его проверки на нуллёвость — у нас может быть конвенция по проверке этих параметров за пределами функции, и компилятор не имеет права вырезать куски кода, считая заведомо, что мы падаем с нулевым указателем.
Да, это стандарт. Но как раз вопрос о том, не чересчур ли это.
lorc
Проблема в том, что в стандарте на ЯВУ не прописан General Protection Fault. Это концепция является практически перпендикулярной к ЯВУ. Могут существовать архитектуры где нет защиты памяти, или где процессор никогда не генерирует исключений при обращении к неправильным адресам. В стандарте прописана модель памяти и эта модель памяти не предусматривает разыменования NULL. Соответственно, когда вы выходите за пределы стандарта — начинаются чудеса.
Честно говоря я не вижу альтернатив. Что может сделать стандарт? Сказать что перед каждым разыменованием указателя, надо проверять его на NULL? Так это ужасный удар по производительности.
ozkriff
Если мы говорим о смене стандарта языка — то делать то что и так пытаются делать современные языки — кодировать null в системе типов. Запретить вообще всем указателям-ссылкам на уровне языка быть null'ами, а там где это реально нужно — программист должен использовать всякие
Option
/?
типы-обертки, которые уже, да, будут требовать явной проверки перед использованием.Так потерь производительности не будет, потому что если там где реально может быть null, проверка обязана быть в любом случае.
alexeykuzmin0
MacIn
А ему и не нужно это понимать, это не его забота. Я говорю о результате, еще раз: если исходный текст предпписывает упасть в GPF, машинный код должен упасть в GPF.
Никто и не говорит, что компилятор «виноват сам по себе».
Нет. Делать то, что предписывает исходный текст. Есть разыменовывание? Разыменовываем. Спекулировать на тему «а вдруг там null» — это черная дыра, антиматерия и хтонический звездец вместе взятые. А вдруг там не null, но тоже недопустимое значение, и что теперь? Вот представим, что в нашей платформе null — это 0. Допустим, у нас в указателе лежит 0x1 — и толку с того, что это не null?
Если уж быть последовательными, давайте не
if (abc == null)
return 0;
do_abc(abc->.a);
использовать, а
if !is_valid_addr(abc)
return 0;
do_abc(abc->.a);
khim
nullptr
. Разименовал — ССЗБ, получи «подарок».А если результат потом никому не нужен? То чего вы хотите — компиляторы тоже умееют. -O0 называется. Но только вы уж выберите чего вы хотите: быстро работающей программы или «делать то, что предписывает исходный код».
Никто таких спекуляций и не делает. Раз это значение кто-то разименовывает, то компилятор знает что там не
nullptr
. Как этого программист добьётся — не задача компилятора выяснять. Разименовали? Неnullptr
точно, можно на это опираться.Более того: в тех местах, где разименования нет — ту же самую информауцию можно донести явно. Как вы думаете — для чего это делается? Чтобы потом делать «Есть разыменовывание? Разыменовываем.»? Ну бред же.
И как, я извиняюсь, ваша,
is_valid_addr
работать будет? Если она будет возврашатьfalse
не только наnullptr
, но и на 1 — так компилятору ещё лучше будет! Он теперь из факта разименования будет получать информацию не только о том, что оказатель не равенnullptr
, но и о том, что это не 1, тоже!0xd34df00d
Нет. Если исходный текст предписывает обратиться к нулевому указателю, то это текст не на языке С, потому что в программе на языке С не бывает неопределённого поведения.
В идеале, конечно, компилятор должен был бы отвергнуть такую программу на этапе компиляции, но, к сожалению, это не столь тривиальная задача в общем случае.
khim
NeverCalled
— и всеUB
из программы пропадут, она станет корректной и будет работать так, как написано.Узнать — есть ли в программе такой модуль компилятор никак не может, но, как уже было 100 раз повторено: если такого модуля нет, то программист — ССЗБ и заслуживает того, что получил…
0xd34df00d
Не на этапе компиляции как одной из фаз сборки программы (вместе с препроцессингом, линковкой и что там ещё в классических книгах), а на этапе компиляции как всего преобразования из исходного текста в готовый бинарь без прогона этого бинаря. Это тоже не очень строгая формулировка, но, надеюсь, доносит мысль.
Надо было бы LTO — ну, значит, с LTO.
khim
NeverCalled
, в том числе, из модуля, который я загружу черезLD_PRELOAD
.LTO нужно специально «заказывать». И, в общем, в этот момент уже сложно выдаватаь подобные ошибки…
0xd34df00d
Если только не
-fvisibility=hidden
. Но да, с дефолтной видимостью, скажем, идеал, увы, недостижим.А так да, я и сам об этом примере подумал, но уже после написания комментария.
Так мы ж про идеал :)
Mingun
-O2
тоже нужно заказывать, никто же не жалуется.Когда сложно компилятору, команде компилятора нужно поднатужиться и допилить компилятор. Когда сложно человеку — нужно переложить это на компилятор, ведь его для этого и придумали :).
khim
Они работают над этим.
Увы, но задачу «подождать часика два, гуляя по Хабру пока какой-нибудь Chrome не скомпилируется с LTO» на компилятор переложить не получится.
Проблема LTO в том, что это реально занимает часы для больших проектов. Потому выдавать ошибки, которые могут быть детектированы только с LTO — неправильно. Ибо никто с LTO не разрабатывает программы, дай бог ночные сборки собирают.
Mingun
Главное, чтобы ошибка была обнаружена во время разработки. В этом смысле обнаружение ее сразу же после написания или через день, в отчете билд-сервера, не слишком отличаются. Ок, пусть эти варнинги будут в ночных сборках.
А проекты, кстати, и необязательно большими могут быть. Странно как-то оправдываться этим, как будто, если что-то не нужно большим проектам, то и маленькие переживут.
khim
Но вообще, теоретически, вы правы — но подобные вещи, скорее, всё-таки специализированные инструменты должны отлавливать. Интересно что на всё это PVC-Studio говорит… хотя думаю что ничего особенного: тут нет никакого криминала, пока вы не соберёте весь проект и не выясните, что функция, которая должна быть вызвана на самом деле нигде не вызвалась…
AllexIn
Я тоже люблю паскаль.
Но я также люблю и С(хотя скорее С++, но не о нём речь). Достаточно просто не писать на нём такого кода.
ozkriff
Проблема только в том что люди, независимо от уровня навыка, время от времени ошибаются и найти настоящий кусок кода на C/C++/т.п. больше, допустим, тысячи строк без UB очень и очень сложно.
AllexIn
От языка это тоже мало зависит.
Поэтому и придумали всякие анализаторы, типа того же PVS-Studio.
ozkriff
Как же не зависит? Вон я выше хотя бы про Rust писал — там UB вне unsafe блоков надо очень постараться что бы получить.
По мне это все-таки костыли, не решающие проблему фундаментально.
kmu1990
Но unsafe там тем нее менее оставили, и если он там есть, значит им кто-то воспользуется, а если им кто-то воспользуется, то кто-то обязательно ошибется, так что проблема как не была фундаментально решена так и осталась.
DarkEld3r
А фундаментально и "не надо". Вон в Java можно сишный код вызывать, но почему-то никто не срывает покровы заявляя, что гарантии джавы ничего не стоят.
Тем более, что выше речь шла о том, что от языка не зависит. Ещё как зависит.
kmu1990
А ещё выше речь шла о том, что всякие анализаторы это костыли не решающие проблему фундаментально. А теперь оказывается, что фундаментально и не надо? В таком случае анализаторы ещё по живут.
ozkriff
Вот же придрался к "фундаментально". :)
Я не имел ввиду что стат-анализаторы не нужны — вон, для ржавчины есть шикарный clippy, которым всем стоит пользоваться регулярно. Он менее ориентирован на поиск именно ошибок в коде, больше на устранение антипаттернов из кода.
Я к тому что по возможности лучше решать проблему на уровне языка. Чем решение ближе к "корю", тем от него больше толку (да, да, ок, даже если решение не убирает вообще все проблемы).
kmu1990
Не к фундаментально, а к тому что анализаторы это костыли.
Есть фундаментальное решение проблемы — Model Checking который строго формально доказывает корректность или находит пример ошибки. Но это только если вы его сможете применить конечно...
Так же и с Rust вы можете сколько угодно его прославлять, но если задача требует использования unsafe, то вы вступаете в мир где Rust уже и не так-то безопасен. И тут вам на помощь приходят "костыли" как вы их назвали.
ozkriff
Я не понял как вышенаписанное оспаривает то что использование анализаторов для поиска ошибок, которые могла бы убрать спецификация языка — костыль.
Вот да, вроде как на практике там как раз все упирается в возможность создать надежную спецификацию, так что тоже не ультимативное решение же.
Если нужно лезть с ffi во внешний мир, то нам, насколько я представляю, никакой язык и никакие анализаторы уже не помогут.
kmu1990
Прям таки никакие анализаторы не помогут? Ну прям ни капельки?
kmu1990
— это сделает спецификацию языка очень большой и сложной для понимания;
— это сделает компилятор сложным и подверженным ошибкам, а компилятор и стандартная библиотека являются источниками доверия;
— не каждый анализ нужен каждому пользователю компилятора.
ozkriff
Хех, хорошо, даже без unsafe все было бы тоже не абсолютно "фундаментальным"- какой бы безопасной мы не написали библиотеку, выполняться оно скорей всего будет поверх глючного железа с глючной ОС.
А ржавый unsafe — прямое следствие системности языка и необходимости взаимодейстоввать с внешним миром. И то что он явный, а не размазан по всему коду, я вижу все-таки огромным плюсом потому, например, что:
kmu1990
Если вы размажете unsafe по всему коду и будет он размазан. Если unsafe запретить, то некоторые вещи сделать будет нельзя, а если не запрещать, то все к ревью сводится. Короче голова всеравно своя быть должна и в C++ и в Rust, и где угодно.
alexeykuzmin0
unsafe инкапсулировать можно. Собственно, в c++ подход предлагается тот же самый — везде писать лишь на том подмножестве c++, которое разрешено c++ core guidelines, а все нарушения надежно инкапсулировать.
kmu1990
А я разве говорил что нельзя? Я просто указал на то, что это зависит от инженера, а не от компилятора.
alexeykuzmin0
ozkriff
Совершенно не спорю, никто о серебрянной пуле не говорит.
Вопрос просто в том что раст убирает неплохую часть геморроя по сравнению с программированием на плюсах (да, добавляя при этом некоторое количество своих собственных "особенностей", ничего не идеально).
Чем лучше язык, тем сложнее в нем говнокодить. Все равно можно, просто сложнее.