Давайте посмотрим вот на такой код:

#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)


  1. Zakyann
    27.09.2017 13:02
    -34

    За что я, среди прочего, не долюбливаю плюсы, предпочитая старый-добрый, тепло-ламповый паскаль :)


    1. lorc
      27.09.2017 15:23
      +2

      В посте приводится пример из чистого C.

      Undefined behavior — как раз то, что позволяет компилировать сишный код в эффективный машинный код.


      1. ozkriff
        27.09.2017 16:59
        +14

        Есть мнение что это уже "слегка" устаревшая точка зрения и с современными знаниями о разработке ЯП и технологиями оптимизации вполне можно иметь системный язык практически без UB. См., например, Rust, где практически все UB возможно только в unsafe блоках.


        1. bfDeveloper
          27.09.2017 17:29

          Ценой усложнения языка. Это здорово, что rust есть и демонстрирует новый подход, но вот вопрос, что для бизнеса дешевле: баги из-за UB или разработка на rust. Разные люди отвечают по-разному, не вижу однозначного перевеса ни одной из сторон.


          1. BlessMaster
            27.09.2017 17:56
            +15

            Сам язык с пользовательской точки зрения — вряд ли сложнее.
            А сравнивать сложность "договориться со строгим компилятором" и "договориться с отладикой, тестированием и фазой луны" — достаточно тяжело сравнивать.


          1. ozkriff
            27.09.2017 18:08
            +8

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


          1. WFrag
            27.09.2017 22:48
            +6

            Усложнение?? Да меня кошмары мучают после того, как я узнал про std::launder!

            Хотя, конечно, Rust-овские lifetime тоже не ягодки. В плане использования ещё не сильно сложно, а вот в плане правильного проектирования — сложно.


      1. MacIn
        27.09.2017 17:57
        +5

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


        1. lorc
          27.09.2017 18:11
          +7

          Возможно данный конкретный пример — это уже перебор. Возможно в данном случае компилятор должен был бы сгенерить вызов функции по адресу NULL. Это более ожидаемое поведение с точки зрения здравого смысла. Но понимаете, очень тяжело засунуть здравый смысл в компилятор.
          А с точки зрения стандарта этот UB ничем не отличается от других UB.


          1. MacIn
            27.09.2017 18:15
            -2

            Но понимаете, очень тяжело засунуть здравый смысл в компилятор

            Не согласен. «Здравый смысл» в рамках компилятора — это сделать отображение ЯВУ на машкод. Так, чтобы получить тот же результат. Если ЯВУ предписывает упасть в General Protection Fault — значит, и машкод должен перейти по адресу 0 и упасть в GPF, а не стереть что-то там, чего исходных текст на ЯВУ не предписывал.
            Абсолютно то же самое с UB ситуациями, когда разыменовывается указатель без его проверки на нуллёвость — у нас может быть конвенция по проверке этих параметров за пределами функции, и компилятор не имеет права вырезать куски кода, считая заведомо, что мы падаем с нулевым указателем.
            Да, это стандарт. Но как раз вопрос о том, не чересчур ли это.


            1. lorc
              27.09.2017 18:22
              +4

              Если ЯВУ предписывает упасть в General Protection Fault — значит, и машкод должен перейти по адресу 0 и упасть в GPF, а не стереть что-то там, чего исходных текст на ЯВУ не предписывал.

              Проблема в том, что в стандарте на ЯВУ не прописан General Protection Fault. Это концепция является практически перпендикулярной к ЯВУ. Могут существовать архитектуры где нет защиты памяти, или где процессор никогда не генерирует исключений при обращении к неправильным адресам. В стандарте прописана модель памяти и эта модель памяти не предусматривает разыменования NULL. Соответственно, когда вы выходите за пределы стандарта — начинаются чудеса.

              Да, это стандарт. Но как раз вопрос о том, не чересчур ли это.

              Честно говоря я не вижу альтернатив. Что может сделать стандарт? Сказать что перед каждым разыменованием указателя, надо проверять его на NULL? Так это ужасный удар по производительности.


              1. ozkriff
                27.09.2017 18:27
                +4

                Честно говоря я не вижу альтернатив. Что может сделать стандарт? Сказать что перед каждым разыменованием указателя, надо проверять его на NULL? Так это ужасный удар по производительности.

                Если мы говорим о смене стандарта языка — то делать то что и так пытаются делать современные языки — кодировать null в системе типов. Запретить вообще всем указателям-ссылкам на уровне языка быть null'ами, а там где это реально нужно — программист должен использовать всякие Option/? типы-обертки, которые уже, да, будут требовать явной проверки перед использованием.


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


                1. alexeykuzmin0
                  28.09.2017 14:44
                  +1

                  Запретить вообще всем указателям-ссылкам на уровне языка быть null'ами, а там где это реально нужно — программист должен использовать всякие Option/? типы-обертки
                  Посмотрите на C++ Core Guidelines, пункт I.12. Собственно, к этому решению c++ и движется. Правда, обертка получается не на уровне языка, а в отдельной библиотеке, и проверка правила на уровне статического анализатора, но это уже детали.


              1. MacIn
                27.09.2017 18:30

                Проблема в том, что в стандарте на ЯВУ не прописан General Protection Fault. Это концепция является практически перпендикулярной к ЯВУ

                А ему и не нужно это понимать, это не его забота. Я говорю о результате, еще раз: если исходный текст предпписывает упасть в GPF, машинный код должен упасть в GPF.
                В стандарте прописана модель памяти и эта модель памяти не предусматривает разыменования NULL. Соответственно, когда вы выходите за пределы стандарта — начинаются чудеса.

                Никто и не говорит, что компилятор «виноват сам по себе».
                Честно говоря я не вижу альтернатив. Что может сделать стандарт? Сказать что перед каждым разыменованием указателя, надо проверять его на NULL? Так это ужасный удар по производительности.

                Нет. Делать то, что предписывает исходный текст. Есть разыменовывание? Разыменовываем. Спекулировать на тему «а вдруг там 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);


                1. khim
                  27.09.2017 18:48
                  +8

                  Я говорю о результате, еще раз: если исходный текст предпписывает упасть в GPF, машинный код должен упасть в GPF.
                  В какой конкретно строке на какой странице стандарта описано GPF и как оно работает? Нет ничего такого в стандарте C++ — зато есть запрет на разименование nullptr. Разименовал — ССЗБ, получи «подарок».

                  Делать то, что предписывает исходный текст. Есть разыменовывание? Разыменовываем.
                  А если результат потом никому не нужен? То чего вы хотите — компиляторы тоже умееют. -O0 называется. Но только вы уж выберите чего вы хотите: быстро работающей программы или «делать то, что предписывает исходный код».

                  Спекулировать на тему «а вдруг там null» — это черная дыра, антиматерия и хтонический звездец вместе взятые.
                  Никто таких спекуляций и не делает. Раз это значение кто-то разименовывает, то компилятор знает что там не nullptr. Как этого программист добьётся — не задача компилятора выяснять. Разименовали? Не nullptr точно, можно на это опираться.

                  Более того: в тех местах, где разименования нет — ту же самую информауцию можно донести явно. Как вы думаете — для чего это делается? Чтобы потом делать «Есть разыменовывание? Разыменовываем.»? Ну бред же.

                  if !is_valid_addr(abc)
                  И как, я извиняюсь, ваша, is_valid_addr работать будет? Если она будет возврашать false не только на nullptr, но и на 1 — так компилятору ещё лучше будет! Он теперь из факта разименования будет получать информацию не только о том, что оказатель не равен nullptr, но и о том, что это не 1, тоже!


                1. 0xd34df00d
                  27.09.2017 21:12
                  +7

                  Я говорю о результате, еще раз: если исходный текст предпписывает упасть в GPF, машинный код должен упасть в GPF.

                  Нет. Если исходный текст предписывает обратиться к нулевому указателю, то это текст не на языке С, потому что в программе на языке С не бывает неопределённого поведения.


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


                  1. khim
                    27.09.2017 22:14
                    +1

                    В идеале, конечно, компилятор должен был бы отвергнуть такую программу на этапе компиляции
                    Это с какого перепугу? Добавьте в программу модуль с глобальным обьектом, конструктор которого вызовет NeverCalled — и все UB из программы пропадут, она станет корректной и будет работать так, как написано.

                    Узнать — есть ли в программе такой модуль компилятор никак не может, но, как уже было 100 раз повторено: если такого модуля нет, то программист — ССЗБ и заслуживает того, что получил…


                    1. 0xd34df00d
                      27.09.2017 22:20
                      +3

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


                      Надо было бы LTO — ну, значит, с LTO.


                      1. khim
                        27.09.2017 22:24

                        Это тоже не очень строгая формулировка, но, надеюсь, доносит мысль.
                        Не доносит. Я могу вызывать NeverCalled, в том числе, из модуля, который я загружу через LD_PRELOAD.

                        Надо было бы LTO — ну, значит, с LTO.
                        LTO нужно специально «заказывать». И, в общем, в этот момент уже сложно выдаватаь подобные ошибки…


                        1. 0xd34df00d
                          27.09.2017 22:30
                          +2

                          Я могу вызывать NeverCalled, в том числе, из модуля, который я загружу через LD_PRELOAD.

                          Если только не -fvisibility=hidden. Но да, с дефолтной видимостью, скажем, идеал, увы, недостижим.


                          А так да, я и сам об этом примере подумал, но уже после написания комментария.


                          LTO нужно специально «заказывать». И, в общем, в этот момент уже сложно выдаватаь подобные ошибки…

                          Так мы ж про идеал :)


                        1. Mingun
                          27.09.2017 22:51

                          -O2 тоже нужно заказывать, никто же не жалуется.


                          И, в общем, в этот момент уже сложно выдаватаь подобные ошибки…

                          Когда сложно компилятору, команде компилятора нужно поднатужиться и допилить компилятор. Когда сложно человеку — нужно переложить это на компилятор, ведь его для этого и придумали :).


                          1. khim
                            27.09.2017 23:15

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

                            Когда сложно компилятору, команде компилятора нужно поднатужиться и допилить компилятор.
                            Они работают над этим.

                            Когда сложно человеку — нужно переложить это на компилятор, ведь его для этого и придумали :)
                            Увы, но задачу «подождать часика два, гуляя по Хабру пока какой-нибудь Chrome не скомпилируется с LTO» на компилятор переложить не получится.

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


                            1. Mingun
                              27.09.2017 23:27

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


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


                              1. khim
                                28.09.2017 00:05

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

                                Но вообще, теоретически, вы правы — но подобные вещи, скорее, всё-таки специализированные инструменты должны отлавливать. Интересно что на всё это PVC-Studio говорит… хотя думаю что ничего особенного: тут нет никакого криминала, пока вы не соберёте весь проект и не выясните, что функция, которая должна быть вызвана на самом деле нигде не вызвалась…


    1. AllexIn
      27.09.2017 18:02

      Я тоже люблю паскаль.
      Но я также люблю и С(хотя скорее С++, но не о нём речь). Достаточно просто не писать на нём такого кода.


      1. ozkriff
        27.09.2017 18:48
        +3

        Достаточно просто не писать на нём такого кода.

        Проблема только в том что люди, независимо от уровня навыка, время от времени ошибаются и найти настоящий кусок кода на C/C++/т.п. больше, допустим, тысячи строк без UB очень и очень сложно.


        1. AllexIn
          27.09.2017 18:52
          -1

          От языка это тоже мало зависит.
          Поэтому и придумали всякие анализаторы, типа того же PVS-Studio.


          1. ozkriff
            27.09.2017 18:55
            +6

            От языка это тоже мало зависит.

            Как же не зависит? Вон я выше хотя бы про Rust писал — там UB вне unsafe блоков надо очень постараться что бы получить.


            Поэтому и придумали всякие анализаторы, типа того же PVS-Studio.

            По мне это все-таки костыли, не решающие проблему фундаментально.


            1. kmu1990
              27.09.2017 23:10

              Но unsafe там тем нее менее оставили, и если он там есть, значит им кто-то воспользуется, а если им кто-то воспользуется, то кто-то обязательно ошибется, так что проблема как не была фундаментально решена так и осталась.


              1. DarkEld3r
                28.09.2017 11:52
                +5

                А фундаментально и "не надо". Вон в Java можно сишный код вызывать, но почему-то никто не срывает покровы заявляя, что гарантии джавы ничего не стоят.


                Тем более, что выше речь шла о том, что от языка не зависит. Ещё как зависит.


                1. kmu1990
                  28.09.2017 17:40

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


                  1. ozkriff
                    29.09.2017 12:00
                    +2

                    Вот же придрался к "фундаментально". :)


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


                    Я к тому что по возможности лучше решать проблему на уровне языка. Чем решение ближе к "корю", тем от него больше толку (да, да, ок, даже если решение не убирает вообще все проблемы).


                    1. kmu1990
                      29.09.2017 12:31
                      -2

                      Не к фундаментально, а к тому что анализаторы это костыли.


                      Есть фундаментальное решение проблемы — Model Checking который строго формально доказывает корректность или находит пример ошибки. Но это только если вы его сможете применить конечно...


                      Так же и с Rust вы можете сколько угодно его прославлять, но если задача требует использования unsafe, то вы вступаете в мир где Rust уже и не так-то безопасен. И тут вам на помощь приходят "костыли" как вы их назвали.


                      1. ozkriff
                        29.09.2017 13:04

                        Не к фундаментально, а к тому что анализаторы это костыли.

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


                        Есть фундаментальное решение проблемы — Model Checking который строго формально доказывает корректность или находит пример ошибки. Но это только если вы его сможете применить конечно...

                        Вот да, вроде как на практике там как раз все упирается в возможность создать надежную спецификацию, так что тоже не ультимативное решение же.


                        но если задача требует использования unsafe

                        Если нужно лезть с ffi во внешний мир, то нам, насколько я представляю, никакой язык и никакие анализаторы уже не помогут.


                        1. kmu1990
                          29.09.2017 23:01
                          -1

                          Вот да, вроде как на практике там как раз все упирается в возможность создать надежную спецификацию, так что тоже не ультимативное решение же.
                          Правда? А вы пробовали? Потому что я пробовал и мне показалось, что проблема в экспоненциальных алгоритмах проверки…
                          Если нужно лезть с ffi во внешний мир, то нам, насколько я представляю, никакой язык и никакие анализаторы уже не помогут.
                          Прям таки никакие анализаторы не помогут? Ну прям ни капельки?


                        1. kmu1990
                          29.09.2017 23:26
                          +1

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


              1. ozkriff
                28.09.2017 12:36
                +4

                Хех, хорошо, даже без unsafe все было бы тоже не абсолютно "фундаментальным"- какой бы безопасной мы не написали библиотеку, выполняться оно скорей всего будет поверх глючного железа с глючной ОС.


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


                • сильно (теоретически) облегачется жизнь оптимизатора в рамках безопасного кода;
                • на уровне проекта unsafe можно просто запретить;
                • можно запретить мерж-боту пропускать меняющие опасные куски кода PR'ы без одобрения кого-то из старших программистов.


                1. kmu1990
                  28.09.2017 17:45

                  Если вы размажете unsafe по всему коду и будет он размазан. Если unsafe запретить, то некоторые вещи сделать будет нельзя, а если не запрещать, то все к ревью сводится. Короче голова всеравно своя быть должна и в C++ и в Rust, и где угодно.


                  1. alexeykuzmin0
                    28.09.2017 17:53
                    +3

                    unsafe инкапсулировать можно. Собственно, в c++ подход предлагается тот же самый — везде писать лишь на том подмножестве c++, которое разрешено c++ core guidelines, а все нарушения надежно инкапсулировать.


                    1. kmu1990
                      28.09.2017 19:36

                      А я разве говорил что нельзя? Я просто указал на то, что это зависит от инженера, а не от компилятора.


                      1. alexeykuzmin0
                        29.09.2017 13:25

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


                  1. ozkriff
                    29.09.2017 12:03
                    +2

                    Короче голова всеравно своя быть должна и в C++ и в Rust, и где угодно.

                    Совершенно не спорю, никто о серебрянной пуле не говорит.


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


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


  1. Nagg
    27.09.2017 13:43
    +18

    Может быть отличной маскировкой бэкдоров :-)


  1. 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"
    
    


    1. 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"
      


  1. CyberKastaneda
    27.09.2017 14:31
    +18

    Это самое undefined из всех UB, которые я когда-либо видел)


    1. Halt
      27.09.2017 16:47
      +3

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


      1. AllexIn
        27.09.2017 18:02
        -2

        Отсутствие break в switch? :))


        1. Halt
          28.09.2017 06:39

          Не, там была именно конструкция вида if (x) { A } else { B } и при этом и А и B выполнялись (в неопределенном порядке).


          1. tyomitch
            28.09.2017 13:11

            Возможно, вы имели в виду: markshroyer.com/2012/06/c-both-true-and-false
            См. тж. подробную хабрастатью.


      1. a1ien_n3t
        27.09.2017 20:41

        Это вроде было гдето в песочнице и там фигурировал флоат и msvc если мне не изменяет память.


        1. a1ien_n3t
          27.09.2017 20:46
          +1

          Нет там было немного другое habrahabr.ru/sandbox/98183


      1. khim
        28.09.2017 12:44
        +1

        Не это, нет?

        Но там всё-таки не совсем две ветви. Там две независимые проверки:

        if (p) {… }
        if (!p) {… }

        Не думаю, что можно сделать так, чтобы обе ветви if'а исполнялись — то что исполнится только одна ветвь определяется строением SSA-дерева.


        1. Halt
          28.09.2017 12:52
          +2

          Нет, вроде бы там было честное условие.

          Не думаю, что можно сделать так, чтобы обе ветви if'а исполнялись — то что исполнится только одна ветвь определяется строением SSA-дерева.
          Не факт. Думаю, что при выполнении loop peeling-а и loop unrolling-а, могут появляться дублирования выражений, которые дальше могут поехать по-разному из-за UB.


  1. nerudo
    27.09.2017 14:38
    +21

    Никто не обещал, что при UB ваш компьютер не взорвется тонной тротила ;)


    1. humbug
      27.09.2017 17:06
      +4

      троллотила


      1. lorc
        27.09.2017 17:55
        +2

        Какой-то древний gcc версии 0.х запускал игру Хайонские Башни, если детектил UB.


        1. Wedmer
          27.09.2017 18:19

          1.17?


        1. Halt
          28.09.2017 06:42
          +4

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


          1. lorc
            28.09.2017 12:56

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


            1. khim
              28.09.2017 13:01
              +1

              Вы оба правы. В стандарте было сказано, что неизвестная #pragma — это UB. Тут подробнее.


  1. evgenWebm
    27.09.2017 16:55
    -4

    Имхо, это можно отнести к багам все таки.
    Хоть и понятно почему так происходит, но это явно не должно так происходить.


    1. 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 не произошло — можно будет о чём-то говорить…


      1. evgenWebm
        27.09.2017 17:57

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

        С того момента, когда ваша программа свернула на «скользкую дорожку» и двинулась по пути, который гарантированно приведёт к Undefined Behavior может происходит всё, что угодно.

        Так может объявим это ошибкой и заставим человека исправить ошибку, а не выполнять изначально не правильный код?

        З.Ы. Программы пишут разные. Большие и маленькие. В маленьких программах найди УБ относительно легко. А если программе 10 лет и пишут ее 100500 программистов? Ситуация такая, Вася налажал, а компилятор пропустил. Не правильно.

        З.Ы. З.Ы. Кстати по умолчанию clang не пропускает и выдает ошибку «Segmentation fault: 11»
        А вот это правильное поведение.


        1. ozkriff
          27.09.2017 18:03
          +7

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


          1. 0xd34df00d
            27.09.2017 21:15
            +3

            Это особенность Тьюринг-полноты, я бы сказал.


            1. ozkriff
              27.09.2017 21:17

              Почему? Я не вижу почему нельзя определить тьюринг-полный язык совершенно без UB.


              1. 0xd34df00d
                27.09.2017 21:23
                +1

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


                1. ozkriff
                  27.09.2017 21:25
                  +1

                  Тогда это уже не просто "полнота по тьюрингу" все-таки, а именно вопрос проектирования "быстрых" языков :)


                  1. 0xd34df00d
                    27.09.2017 21:32

                    Интересно, насколько реально доказывать все такие требующие производительности случаи явно в каком-нибудь Coq, экспортировать в сишечку, никогда этот С-код руками больше не трогать и лишь дёргать его из safe-языка.


                    1. ozkriff
                      27.09.2017 21:36

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


                      1. 0xd34df00d
                        27.09.2017 21:42

                        C compilation path там вроде как deprecated. Да и из хаскеля хреноватый прувер, на самом деле.


                        А так-то мой вопрос был скорее про готовность такого пайплайна к продакшену (или продакшена к такому пайплайну, если хотите).


                1. aamonster
                  27.09.2017 22:44

                  Да ладно, есть же доказательство "мамой клянусь!" :-)
                  Хотя это уже будет не Си, но, наверное, на нынешнем этапе это правильнее (нет необходимости до упора оптимизировать всё — достаточно найти бутылочное горлышко)


          1. norlin
            27.09.2017 22:06
            -1

            большую часть UB можно засечь только во время выполнения кода

            Эм. Тогда надо вызывать NULL и честно крашиться.


        1. AllexIn
          27.09.2017 18:04

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


        1. atrosinenko
          27.09.2017 18:12
          +3

          Компилятор пусть лучше выдаст ошибку компиляции.

          Насколько я понимаю, проблема в том, что статически все UB не отловить. Возможно, вы имеете в виду, что, например, разыменование указателя должно быть разыменованием (и падать на NULL), но что, если это обломает компилятору какую-нибудь хорошую оптимизацию. Но это — ладно — просто доопределим поведение. А если *((int*)rand()) — как здесь гарантированно упасть?


          А если программе 10 лет и пишут ее 100500 программистов?

          Можно попробовать Undefined Behavior Sanitizer в Clang или GCC. Но он отлавливает только то, что реально произошло в процессе работы, и не уверен, что весь UB можно отловить хотя бы в run-time.


        1. khim
          27.09.2017 18:20
          +5

          Так может объявим это ошибкой и заставим человека исправить ошибку, а не выполнять изначально не правильный код?
          Это будет другой язык с другой спецификацией. Вот тут человек пробовал что-то такое изобразить, но быстро выяснилось, что создать подобную спеку — это очень и очень непростая работа.

          Ситуация такая, Вася налажал, а компилятор пропустил. Не правильно.
          Строго говоря все UB вы никогда не отловите. Например для отлова обращений по «провисшему» указателю (у которого кто-то вызвал free, но продолжает использовать) вам фактически придётся сделать GC — и то не факт что поможет (подумайте что будет если на эти самые «направильно удалённые» элементы кто-нибудь будет ссылаться в XOR-связном списке).

          И? Что теперь? Заведём в дополнение к UB ещё классификацию «хорошие UB» и «плохие UB»? Кто границу будет проводить? И как?

          З.Ы. З.Ы. Кстати по умолчанию clang не пропускает и выдает ошибку «Segmentation fault: 11»
          «По умолчанию» — это как? Без оптимизаций? Даже древний, как говно мамонта clang 3.0 ведёт себя так, как описано в статье.

          А вот это правильное поведение.
          У программы, вызывающей UB любое поведение правильно — по определению.


          1. evgenWebm
            27.09.2017 19:48
            -1

            По умолчанию clang не пропускает и выдает ошибку. Имхо считаю тему закрытой.


            1. khim
              27.09.2017 20:19
              +2

              По умолчанию clang не пропускает и выдает ошибку. Имхо считаю тему закрытой.
              Можете продолжать считать, что сотни тысяч программистов «шагают не в ногу», а вы один — в ногу. Ваше право.

              Hint: «по-умолчанию» clang собирает всё без оптимизаций если не пользоваться билд-системами. Но если использовать CMake, AutoConf или что-нибудь подобное — то будет использоваться -O2 со всеми вытекающими… И вы не поверите — но реальные проекты редко кто собирает без билд-систем…


              1. evgenWebm
                27.09.2017 21:24
                +2

                Я не писал, что тысячи программистов… или что я один умней других.
                Я высказал свое мнение.

                Но, признаю свою не правоту. Разобрался.
                Действительно недетская ситуация получается и сделать что то сложно.


    1. qadabr
      27.09.2017 21:31
      -2

      Нет, это не баг, а неопределенное поведение. Если бы программист написал так:

      static Function Do = NULL;
      то функция EraseAll и не вызвалась бы


      1. 0xd34df00d
        27.09.2017 21:32
        +2

        Почему?


      1. DistortNeo
        27.09.2017 21:58
        +3

        Нет. Она бы не вызвалась, если бы программист написал


        if (Do != nullptr)
            return Do();


  1. 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 /");
    ^


    1. slonopotamus
      27.09.2017 20:01
      +2

      Я правильно понял, что clang смог определить что ему на вход дали программу с UB, но вместо того чтобы пожаловаться на это, сгенерировал дурь?


      1. khim
        27.09.2017 20:32
        +5

        Неправильно. Вы статью-то читали? clang не пытался определять — есть в программе UB или нет. Это не его задача. Он провёл анализ и выяснил, что указатель, в данной программе, может быть равен либо nullptr, либо EraseAll. После чего выяснил, что в единственном месте, где этот указатель используется — он может быть использован без вызов UB только в случае если он, каким-то образом, стал равным EraseAll. Стало быть думать и гадать не нужно — а можно сразу вызвать EraseAll.

        Представьте что вы используете эту программу не как главную программу, а как динамическую библиотеку. Тогда — вы не имеете права вызывать main без предварительного вызова NeverCalled (иначе вы напоретесь на UB). А после вызовы NeverCalled у вас main будет вызывать EraseAll — гарантированно! Так зачем делать лишние телодвижения?

        В том-то и дело, что это вам кажется, что компилятор «сгенерировал дурь». С точки зрения же языка — всё правильно: любое использование этого кода не вызывающее UB будет работать так же, как и раньше — а что будет делать этот код, если его будут использовать неправильно, вызывая UB — разработчиков компилятора не волнует от слова «совсем».


        1. michael_vostrikov
          27.09.2017 20:59
          -4

          Речь о том, что точка зрения языка нелогична.


          1. 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… но постойте: компилятор — он же не человек, он смысла в вашей программе не ищет, он просто исходит их определённых правил, описанных в спецификации языка!


            1. michael_vostrikov
              27.09.2017 21:42
              -1

              И эмоции у меня те же самые. Почему компилятор использует plain_multiply, если InitEngine нигде не вызывается? В исходном коде не указано, что ее надо использовать, значит компилятор сделал не так как надо.


              1. khim
                27.09.2017 21:57
                +4

                Почему компилятор использует plain_multiply, если InitEngine нигде не вызывается?
                Как это «не вызывается»? Она в другом модуле вызывается, разумеется. А иначе как бы эта программа работала 30 лет назад со всеми этиме Weitek'ами и Motorolla'ми?

                И я вас уверяю — никто из тех, кто обнаружит, что из код стал работать в 10 раз быстрее не будет задумываться над тем, что InitEngine вызывается из другого модуля и понять, что cube не вызывается до InitEngine никак нельзя и, стало быть, компилятор, по вашей логике, не имеет права ничего никуда тут подставлять!

                В этом-то и беда: когда подобные оптимизации срабатывают нормально (а это 99% случаев), то никто и не задумывается за счёт чего, а когда, в одном случае из 100, что-то идёт не так — то поднимается вселенский вой.


                1. michael_vostrikov
                  27.09.2017 22:26

                  Она в другом модуле вызывается, разумеется.

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


                  никто из тех, кто обнаружит, что из код стал работать в 10 раз быстрее не будет задумываться над тем, что InitEngine вызывается из другого модуля

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


                  1. khim
                    27.09.2017 22:41
                    +3

                    Если он про другой модуль не знает, то это некорректная оптимизация.
                    Компилятор про другой модуль не знает, но может доказать что в корректной программе на C или C++ такой модуль есть и он вызывает-таки InitEngine.

                    А чтобы компилятор смог провести оптимизацию, достаточно помечать такие функции особым образом, чтобы он знал, что она точно вызывается где-то снаружи.
                    Но… зачем? Компилятор и так знает, что это происходит — в противном случае программа некорректна!


                    1. Mingun
                      27.09.2017 22:46

                      Явное лучше неявного — уж сколько про этот принцип твердят...


                      1. khim
                        27.09.2017 22:47
                        +1

                        Вы C/C++ с Python'ом случайно не путаете? Это — разные языки и у них разные подходы…


                        1. Mingun
                          27.09.2017 23:23

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


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

                          Кстати, такая пометка уже есть — атрибут hidden, просто по умолчанию все функции видимы и их нужно явно скрывать. Сейчас в новых языках люди уже поняли, что проще делать наоборот.


                          1. khim
                            27.09.2017 23:56
                            +1

                            Кстати, есть в стандарте что-то про видимость функций и управление ею?
                            Много всякого. static, inline, анонимные namespace. Но да, нелостаточно подробно. Может с модулями получше будет.

                            Кстати, такая пометка уже есть — атрибут hidden, просто по умолчанию все функции видимы и их нужно явно скрывать.
                            Hidden — это не на том уровне. То, что вы имеете в виду — это static.

                            Здесь NeverCalledне static и, соотвественно, по умолчанию вызывается извне.


                            1. Mingun
                              28.09.2017 20:46

                              Не, я как раз имел ввиду аналог __attribute__((visibility ("hidden"))) в GCC, только на уровне стандарта. Чтобы управлять видимостью функций во всем бинаре, а не в отдельном объектнике. Модули — это C++, а на C ничего нет и завозить не собираются?


                              Сейчас не static функцию можно вызвать как из другой единицы трансляции, объявив ее через extern, так и из другого бинаря, если этот загрузить, как библиотеку. А можно ли оставить только первый вариант использования, а второй запретить? GCC-шный атрибут, как я понимаю, это и делает.


                              1. khim
                                28.09.2017 21:25
                                +1

                                Вы перепрыгиваете этапы. Вы пока не обьяснили — как вы собираетесь бороться хотя бы с тем, что не-static функцию инициализации могут не вызвать, а уже хотите что-то делать с динамическими библиотеками, о которых C/C++ вообще ни сном ни духом.

                                Модули — это C++, а на C ничего нет и завозить не собираются?
                                А ему и не нужно. Заголовочных файлов хватает. Модули же будут «работать» на этапе статической сборки, а не динамической, всё равно.


                    1. michael_vostrikov
                      27.09.2017 23:13

                      Как он доказал наличие другого модуля в коде из статьи? Там ведь его нет.


                      Но… зачем? Компилятор и так знает, что это происходит — в противном случае программа некорректна!

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


                      1. khim
                        27.09.2017 23:18
                        +3

                        Как он доказал наличие другого модуля в коде из статьи?
                        Вы статью читали? Там написано.

                        Там ведь его нет.
                        Потому что код программы в статье не является корректной программой на C. А раз так — то любое поведение допустимо, в том числе то, которое изибразил clang.

                        Речь о том, что он знает неправильно, что его правила определения правильности нелогичны.
                        Его правила полностью согласованы со стандартом. Хотите других правил — меняйте стандарт.


                        1. michael_vostrikov
                          28.09.2017 09:26

                          Потому что код программы в статье не является корректной программой на C… Его правила полностью согласованы со стандартом.

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


                          1. khim
                            28.09.2017 12:46
                            +7

                            Вы меряете логичность программы стандартом.
                            А чем ещё его мерить? «Здравый смысл» у всех разный, только стандарт — один на всех.


                            1. michael_vostrikov
                              28.09.2017 13:18

                              А в соответствии с чем писали стандарт?


                              1. lorc
                                28.09.2017 14:00
                                +1

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


                                1. michael_vostrikov
                                  28.09.2017 17:32

                                  Так ведь есть способы исправить ситуацию из статьи, не повлияв на оптимизации и остальное. Просто надо сделать кое-что более явным.


                              1. khim
                                28.09.2017 14:14
                                +1

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

                                И причина та же — «здравых смыслов» много, а законы нужны одни на всех.


                        1. tyomitch
                          28.09.2017 13:39
                          +2

                          Его правила полностью согласованы со стандартом. Хотите других правил — меняйте стандарт.

                          На самом деле нет: «хотите других правил — пишите на другом языке».

                          Для сравнения: у Питона, в котором «явное лучше неявного», вообще нет стандарта, а значит нет вообще никаких гарантий, что ваша программа продолжит работать так, как вы задумали, после переноса на другую платформу или на другую версию транслятора.
                          Си же гарантирует, для программ без UB, корректную работу всюду и всегда.


                      1. mayorovp
                        28.09.2017 08:54
                        +1

                        Как он доказал наличие другого модуля в коде из статьи? Там ведь его нет.

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


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


                        1. michael_vostrikov
                          28.09.2017 09:06

                          Так в том и суть, что раз логические выводы компилятора расходятся с положением вещей, значит логика у него неправильная. Никто же не говорит, что пример из статьи не соответствует стандарту. Говорят, что стандарт нелогичный, раз допускает такое толкование исходников.


                          1. lorc
                            28.09.2017 13:10

                            Стандарт не может покрыть все возможные случаи. Точнее, наверное, он может покрыть, но получится что-то вроде Ады.

                            Стандарт описывает правильно написанные программы. Описывает явные ошибки. И стандарт явно указывает на вещи, которые выходят за его пределы.

                            Вот как бы вы предложили изменить стандарт, что бы избежать описанного в статье поведения?


                            1. khim
                              28.09.2017 13:14

                              Стандарт не может покрыть все возможные случаи. Точнее, наверное, он может покрыть, но получится что-то вроде Ады.
                              И это плохо, потому что…

                              Вот как бы вы предложили изменить стандарт, что бы избежать описанного в статье поведения?
                              Достаточно сделать обращение по nullptr не «undefined behavior», а «unspecified» behavior — и всё. Можно даже «implementation-specific» его сделать, если хочется гарантированного падения по GPF в подобных случаях.


                              1. lorc
                                28.09.2017 13:30
                                -1

                                И это плохо, потому что…

                                Ну я не могу прямо вот так сказать чем это плохо. Почему на Аде пишут в основном только военные, а на С — вообще все?
                                Почему языки с плохим дизайном становятся популярными?
                                Можно ли создать безопасный, удобный, но близкий к железу язык? (да, я слышал про Rust)

                                Мне кажется, что большинство программистов предпочтет писать на небезопасном языке, чем барахтаться в Turing tar-pit.


                                1. khim
                                  28.09.2017 14:28
                                  +3

                                  Почему на Аде пишут в основном только военные, а на С — вообще все?
                                  Потому что на C был написан UNIX, с которого были, во многом, стянуты DOS и Windows, тоже написанные на C, а также его поддержал GCC, который усилиями Cygnusа проник в embedded.

                                  К свойствам собственно языка это имеет не так много отношения.

                                  Почему языки с плохим дизайном становятся популярными?
                                  А почему операционки с плохим дизайном оказываются популярными? Потому что они оказываются «готовыми к употреблению» быстрее, чем языки с хорошим дизайном. Это уже обсосано 100 раз

                                  Мне кажется, что большинство программистов предпочтет писать на небезопасном языке, чем барахтаться в Turing tar-pit.
                                  Большинство программистов пишут на том языке, на котором могут писать. И используют ту базу данных, которую могут использовать. И так далее.


                                  1. tyomitch
                                    28.09.2017 17:52

                                    DOS и Windows, тоже написанные на C

                                    Вообще-то MS-DOS был написан на ассемблере, от первых до последних версий.

                                    Ну и, вообще-то, «стянуто с UNIX» в них довольно мало. Что там, собственно, «стянуто», кроме иерархической ФС и BSD-сокетов?


                                    1. khim
                                      28.09.2017 18:16
                                      +2

                                      Вообще-то MS-DOS был написан на ассемблере, от первых до последних версий.
                                      Первые версии — да, последние — уже частично на C были. Ядро, впрочем, до конца было на ассембелере.

                                      Что там, собственно, «стянуто», кроме иерархической ФС и BSD-сокетов?
                                      А чего, собственно, в MS-DOS, кроме иерархической ФС, вообще есть?

                                      Не забывайте всё-таки, какую OS Microsoft создал первой. Hint — это был совсем даже не MS-DOS.


                  1. khim
                    27.09.2017 22:46
                    +5

                    Думаю в комментариях достаточно задумавшихся.
                    Я бы сказал, что в конмментариях достаточно удивившихся. Подавляющее большинство, я боюсь, считает, что потимизация — это когда компилятор берёт программу, «понимает» что она делает и создаёт другую, эквивалентноую — но «лучше».

                    Что и близко не похоже на то, что делает компиляторе. На самом деле «понять» программу он не умеет, но может «повертеть» её — в соотвествии со спецификациями. И тут для него UB является «путеводной звездой»: если программа, не вызывающая UB работает также, как и после, то преобразование — хорошее, правильное, годное. Нет — значит нет…


                1. michael_vostrikov
                  27.09.2017 22:56
                  +1

                  и понять, что cube не вызывается до InitEngine никак нельзя

                  Кстати, слышал звон что в C++ собираются добавить модули, да все никак не соберутся. Мне видится здесь прямая связь. Если бы было понятие "конструктор модуля", то можно было бы гарантировать вызов InitEngine перед остальными. То есть проблема не в возможности оптимизаций NULL, а в отсутствии средств сообщить поток выполнения компилятору, вот он и вынужден предполагать, что программист не допустит NULL-ов где не надо.


                  1. DistortNeo
                    27.09.2017 23:18

                    И чем гипотетический конструктор модуля будет отличаться от конструктора глобальной переменной?


                    1. khim
                      27.09.2017 23:20
                      +3

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


                      1. Kobalt_x
                        28.09.2017 05:59

                        Ну все же не совсем в любом, в рамках одного модуля в порядке определения, а вот модули уже как получится


                        1. khim
                          28.09.2017 12:36
                          +1

                          Замечание верное «не греющее»: главное, чего хочется от инициализатора модуля — это чтобы он, собственно, инициализировал модуль (удивительное желание, правда?), то есть отрабатывал до любой функции модуля. А это значит что он должен сработать то того, как другие инициализиторы, которые потенциально могут вызывать эти функции, отработают. С глобалами этого сделать, увы, нельзя, с TPU-файлами в Turbo Pascal 4.0, вышедшем 30 лет назад — можно.


                          1. Kobalt_x
                            28.09.2017 13:02

                            ну в gcc есть __attribute__(constructor) который делает вроде именно то что вы хотите


                            1. khim
                              28.09.2017 13:08
                              +1

                              Нет, он, к сожалению, делает ровно то же самое, что и глобалы в С++. Собственно «за сценой» они транслируются в один и тот же код.


        1. Mingun
          27.09.2017 21:01
          +1

          C вашим объяснением никто не спорит. Но и компилятору никто не мешает вывести информацию о неочевидном инлайнинге без всяких хитрых ключей, с которыми потонешь в тоннах слишком подробного вывода. Просто как предупреждение: "мил человек, обрати внимание, что тут я делаю то-то, может, это не совсем то, что ты хочешь".


          И сразу по поводу мантры, которая регулярно в таких обсуждениях всплывает — не требуется ловить ВСЕ UB. Достаточно ловить и предупреждать хотя бы о некоторых. Даже если (о ужас) они могут оказаться ошибочными.


          1. ozkriff
            27.09.2017 21:07

            Так некоторые уже ловятся. А в статье просто пример где не ловится.


            1. Mingun
              27.09.2017 21:17

              Просто с позицией


              разработчиков компилятора не волнует от слова «совсем».

              как-то сложно поверить, что будет когда-то ловится. Типа, ешьте что дают. Такими методами можно потерять лояльных пользователей, которые уйдут на более дружелюбные языки. Уже монополию C Rust пошатывает и я уверен, дальше будет только больше.


              Так что слова "не волнует", "совсем" и иже с ними стоит забывать. Время изменилось. Сейчас прогресс настолько далеко шагнул вперед, что само-собой разумеющемся предполагается, что именно компилятор и должны волновать подобные вопросы. В конце концов, человек работает для компилятора или компилятор для человека?


              1. khim
                28.09.2017 12:56

                Уже монополию C Rust пошатывает
                И в случае победы разработчики LLVM'а заменят разработчиков LLVM'а!

                Вы точно уверены, что это должно их испугать?


                1. splav_asv
                  28.09.2017 13:06
                  +2

                  Rust пытается двигаться в сторону альтернативного кодогенератора (для отладочных сборок по крайней мере). Не устраивает скорость и заточенность на C-подобные языки.


                  1. 0xd34df00d
                    28.09.2017 20:51
                    +1

                    Было бы очень интересно почитать разработчиков Rust'а на эту тему (или просто знающих людей). В частности, что именно в заточенности на С-подобные языки их не устраивает.


                    1. DarkEld3r
                      28.09.2017 21:52
                      +1

                      Только такое видел, но при беглом просмотре не нашёл ничего про "заточенность на С".


                      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


                    1. splav_asv
                      29.09.2017 09:09
                      +1

                      Про заточенность на C-подобные языки — мелькало в обсуждении некоторых багов. Как главная мотивация не выступает, просто иногда доставляют неудобство некоторые моменты. Если еще раз наткнусь — постараюсь кидать сюда ссылки.


                    1. ozkriff
                      29.09.2017 12:06
                      +1

                      https://www.reddit.com/r/rust/comments/5u3vrq/undefined_behavior_unsafe_programming/ddr5fd8/ тут вот ссылка на два косяка, которые, как я понимаю, тоже относятся к "заточенности LLVM на С-подобные языки".


          1. 0xd34df00d
            27.09.2017 21:19
            +5

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

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


          1. khim
            27.09.2017 21:37
            +3

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

            И? Как должен вести себя компилятор? Если мы инлайним функцию с названием NeverCalled — предупреждать? Или как?

            Достаточно ловить и предупреждать хотя бы о некоторых. Даже если (о ужас) они могут оказаться ошибочными.
            Отлично. Вот вам две программы. Обьясните на основании чего в одной из них будет выдана диагностика, а в другой нет. Вариант, при котором мне про InitEngine будут вопить не предлагать, пожалуйста — в этой программе нет никакого криминала. Ну то есть совсем никакого.

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


            1. Mingun
              27.09.2017 22:44
              -1

              А как он до этого догадаться должен?

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


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

              В обоих случаях должны быть выданы. Эта ситуация легко детектируется по паттерну, который я только что описал, и здесь лучше перебдеть, чем недобдеть. Во втором случае вы же сами и решение описали — раз теперь бекенд только один, заменяем на его прямые вызовы и устраняем варнинг. Заодно и поддержка дальнейшая упрощается. Профит. Для первой ситуации исправляем ошибку. Опять профит. 2 профита против одного полупрофита — по-моему, довольно неплохо.


              А в оригинальном примере в другом модуле может быть глобальный обьект

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


              1. khim
                27.09.2017 22:49
                +1

                Остается еще динамическая линковка, когда кто-то загружает ваш модуль, как библиотеку и зовет эту злополучную NeverCalled, но я не вижу причин, по которым компилятор должен считать это событие более вероятным, чем то, что допущена ошибка, о которой он должен сообщить.
                Это не ошибка, так как программа может быть кореектной. А предупреждения — штука ой какая непростая, тут люди из PVS-Studio каждую неделю про это статьи пишут…


                1. Mingun
                  27.09.2017 23:03

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


                  1. khim
                    27.09.2017 23:25
                    +2

                    В предположении, что ее будут грузить как библиотеку и вызывать функции в определенном порядке.
                    Не обязательно. Как и написано в статье: вы можете добавить другой .cc файл в вашу программу, который вызовет из конструктора глобального обьекта NeverCalled. Это сделает вашу программу корректной. Хотя она по прежнему будет вызывать rm -rf.

                    Для этого примера выдать варнинг было бы лучше.
                    Это уже другой вопрос — я вот не уверен. В другом очень похожем примере (причём на практике втревающемся в 100 раз чаще) варнинг будет выглядеть глупо «мы обнаружили, что в вашей программе переменная всегда инициализируется фиксированным значением» — «что мне прикажете с этим делаеть?».


                    1. Mingun
                      27.09.2017 23:34

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


                      «что мне прикажете с этим делаеть?»

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


                      1. khim
                        28.09.2017 00:01
                        +3

                        Вот когда файл с конструктором и глобальным объектом добавят, тогда и предупреждения не должно быть, с этим я не спорю.
                        Тут даже не модуль телепатии нужен, а целый «хрустальный шар». Один и тот же обьектный файл может включаться в разные бинарники — в одном такой обьект будет, в другом — не будет… Откуда компилятору-то знать?

                        Если у компилятора здесь возникают вопросы
                        У компилятора, как раз, никаких вопросов не возникло. Всё сделано в соотвествии со спеками и ни о чём беспокоиться не нужно — хоть ты -Wextra-extra-extra задай…


              1. khim
                27.09.2017 23:00
                +4

                В обоих случаях должны быть выданы.
                Это мне на корректный код, который даже ни по одному критерию не является ни разу «проблемным» (с точки зрения корректности, не скорости) будут предупреждения выдавать? Нифига себе.

                Во втором случае вы же сами и решение описали — раз теперь бекенд только один, заменяем на его прямые вызовы и устраняем варнинг.
                А если мы потом захотим это на каком-нибудь AVRе запускать? Со странным Waitek-style сопроцессором?

                Заодно и поддержка дальнейшая упрощается.
                Поддержка упростится до тех пор, пока мы не захотим разные Engine'ы снова оживить. И почему вдруг корректный, роботающий код нам нужно будет переделывать чтобы компилятор «успокоить»? Не много ли он на себя берёт?

                Как я уже написал выше, компилятор знает, вызывает кто-то эту функцию или нет в собираемой программе.
                Ни черта компилятор не знает, уж извините. Модули опять отложили (может в C++20 будут) потому о том, есть ли в программе конструкторы вызывающие NeverCalled компилятор может только догадываться. UB здесь является путеводной звездой: если что-то позволяет нам избежать UB только одним способом — значит, в привильной программе, этот способ и будет вызван.


                1. Mingun
                  27.09.2017 23:13

                  Ну когда захотите, тогда и вернете на место. В конце концов, вы же уже переписали код и убрали зависимость от engineType, почему остановились на этом? Для AVR у вас 2 варианта — либо там точно такая же функция используется, либо своя. Без реинкарнации разных Engine это делается препроцессором и там опять одна функция. В обоих случаях для поддерживаемости лучше записать прямо, чего вы хотите, а не полагаться на оптимизации компилятора. Или отладочную версию программы без оптимизаций вы собирать не собираетесь?


                  Ни черта компилятор не знает, уж извините.

                  Знает, знает. Инструкцию call он не сможет сгенерировать, если не будет этого знать. Разумеется, он не знает, действительно ли вызовут функцию, но знает, возможно ли это в принципе или нет. Это задача об определении мертвого кода, с которой компиляторы уже пару десятков лет успешно справляются.


                  1. khim
                    27.09.2017 23:40
                    +3

                    В конце концов, вы же уже переписали код и убрали зависимость от engineType, почему остановились на этом?
                    Потому что переписали 100 строк разных умножений/сложений/вычитаний. А вы предлагаете всю библиотеку переделать ради того, чтобы компилятор «успокоить». Притом что сейчас — всё работает.

                    Без реинкарнации разных Engine это делается препроцессором и там опять одна функция.
                    Не обязательно. На 80386 выбор между 80387 и 3167 выполнялся в рантайме. Выбор между MaverickCrunchем, softfp и VFP тоже может оказаться полезным выбирать в рантайме.

                    То что у вас в конкретном бинарнике есть только один вариант не означает, что никаких других из этих исходников не собирают…

                    Разумеется, он не знает, действительно ли вызовут функцию, но знает, возможно ли это в принципе или нет.
                    Если фукнция эскпортирована (а NeverCalled в оригинальном примере экспортирована) — то это возможно. Потому он её и оставляет, хоть и пустую…


                    1. Mingun
                      28.09.2017 20:28
                      -1

                      Так вы все-таки определитесь, есть у вас выбор в рантайме, или вы все выпилили и оставили одну реализацию.


                      Если выбор в рантайме есть — то это не та ситуация, которую мы обсуждаем.


                      Если внутри вашего бинаря функция настройки (InitEngine или NeverCalled) вызывается — это опять не та ситуация, мы рассматриваем ситуацию, когда она не вызывается изнутри бинаря (о вызовах снаружи далее).


                      Если же InitEngine таки не вызывается внутри этого бинаря, это значит одно из двух:


                      • вы специально удаляли код ее вызова из вашего бинаря (помним, мы рассматриваем ситуацию, когда вызова InitEngine в бинаре нет). Что мешало пойти дальше и удалить теперь уже бесполезный косвенный вызов в cube, вы же все равно правили связанный код?
                      • она вызывалась только снаружи. Что мешало теперь сделать функцию пустой, ведь от того, вызывают ее или нет, ничего не меняется, и удалить теперь уже бесполезный косвенный вызов в cube?

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


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


                      Но когда он не знает, вызывали функцию или нет, он должен отреагировать на это предупреждением. Заметьте, я не говорю, что он должен как-то менять свое поведение и генерируемый код. Пусть просто предупредит. Если вы железобетонно уверены, что тут все в порядке, просто отключите предупреждение в этом месте.


                      То что у вас в конкретном бинарнике есть только один вариант не означает, что никаких других из этих исходников не собирают…

                      Не понял, что вы хотите этим сказать.


                      Если фукнция эскпортирована (а NeverCalled в оригинальном примере экспортирована) — то это возможно. Потому он её и оставляет, хоть и пустую…

                      О чем я и говорю. Изнутри получившегося бинаря она не вызывается, о чем компилятору точно известно (после линковки), причем даже без всяких LTO. А ее код заинлайнился в том место, которое точно вызывается, и это тоже известно — что заинлайнилось, куда, даже строчка точно указывается, как показывают в комментарии. Поэтому у компилятора достаточно информации, чтобы однозначно выявить нестандартную ситуацию и выдать предупреждение. Предупреждение — не ошибка, оно отключается. Предупреждение не обязывает компилятор менять уже сгенерированный код, если вы об этом в предыдущей не понятой мною фразе.


                      1. 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.


                        1. 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 файлов нерасширяемый и его ни разу не приходилось расширять. В любом случае, это решаемая техническая проблема).


                          1. khim
                            29.09.2017 00:25
                            +4

                            Противоречие в том, что странные преобразования, приводящие к неочевидным багам, компилятор делает для оптимизации, а рядом вы забиваете на нее большой болт и пытаетесь в рантайме выбрать из одного варианта. Зачем?
                            Не «зачем», а «почему». Потому что мне так удобнее. Если код нужно будет исполнять на системах, где возможны несколько несовместимых сопроцессоров (да хотя бы выбирать между SSE4.2 и AVX'ом), то это может быть полезно переключать в рантайме. А если мы тот же код компилируем под одну платформу (скажем прошивка под конкретную железку) — то компилятор всё заинлайнит и все довольны.

                            Почему в бинарнике для этой платформы нельзя выкинуть неиспользуемый код для других платформ? Зачем он там нужен?
                            В бинарнике, как мы убедились, его и нет. Он есть в исходниках — но это уже совсем другая история.

                            Да почему в процессе компиляции файла module.cc-то???
                            Потому что там и только там используется компилятор C/C++.

                            Я вам уже сколько сообщений подряд твержу, даже в скобках детально пояснять все стал. Не в процессе компиляции, а в процессе линковки всех объектников программы!
                            В процессе линковки всех объектников компилятор не участвует. Этим занимается совсем другая программа — линкер. О 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 даже читать не будет?

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

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


              1. Kobalt_x
                28.09.2017 06:02

                Знает он только если всё собирается с lto если собираются отдельные модули то ничего неизвестно


                1. Mingun
                  28.09.2017 20:35

                  Это почему это? В каждом собираемом объектнике наружу торчат "порты", в которые нужно подключить "порты" других объектников, что и делается на этапе линковки. Если какие-то порты оказались незаполненными — это ошибка линковки — undefined symbol называется. Вот если мы заюзали NeverCalled или InitEngine в одном из таких портов, это автоматически означает, что они используются, а если нет — автоматически, что не используются.


                  1. khim
                    28.09.2017 21:21
                    +1

                    Вот если мы заюзали NeverCalled или InitEngine в одном из таких портов, это автоматически означает, что они используются, а если нет — автоматически, что не используются.
                    Серьёзно? Берём модуль из статьи, добавляем следующее (в другом файле):
                    void NeverCalled();
                    
                    void SomeoneIsAnIdiotBwahaha() {
                      NeverCalled();
                    }
                    
                    И вуаля: согласно вашему критерию — мы «заюзали» NeverCalled, все «порты» «запортили» и вообще всё стало ништяк. Вот только неопределённое поведение никуда не делось…


                    1. Mingun
                      28.09.2017 21:53
                      -2

                      Функция используется другой функцией? Используется. То, что это вторая функция не вызывается — уже другая проблема. Предполагаем, что компилятор ее не выкинул (если выкинул, как мертвый код — возвращаемся к ситуации, когда ее совсем не было, с которой уже разобрались). Хотя и это проблема решаема, нужно только знать, кто что вызывает. Компилятор граф вызовов строить может.


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


                      1. khim
                        29.09.2017 00:36
                        +1

                        То, что это вторая функция не вызывается — уже другая проблема.
                        Нет — это та же самая проблема. Вы жалуетесь, что компилятор порождает код, который предполагает что определённая функция будет вызвана — но вы не можете определелить — будет ли она вызвана на самом деле! В конце-концов там может меню быть и инструкция для оператора «перед тем, как запускать вычисления зайдите в меня „выбор FPU“ и укажите правильный сопроцессор, в противном случае программа может не работать». И как вы это всё будете отслеживать в вашем линкере?

                        Компилятор граф вызовов строить может.
                        Он не может даже понять — вызовется ли та или иная функция или нет. проблема остановки — и это если ещё оператор не задействован! В противном случае вопрос провоцирования UB и вовсе нерешаем…

                        Тогда как я говорю о выявлении хотя бы очевидных случаев, как в статье.
                        «Неочевидный случай как в статье» встречается дай бог к 0.01% случаев использования подобной оптимизации. Если вам охота тратить время и силы на решение этой, высосанной из пальца, в общем-то, проблемы — дерзайте. Но у разработчиков компиляторов другие, более насущные, проблемы есть, чем гоняться за редкими багами в криво написанных программах с ошибками.

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


                      1. tyomitch
                        01.10.2017 00:41
                        +1

                        Функция используется другой функцией? Используется. То, что это вторая функция не вызывается — уже другая проблема. Предполагаем, что компилятор ее не выкинул (если выкинул, как мертвый код — возвращаемся к ситуации, когда ее совсем не было, с которой уже разобрались). Хотя и это проблема решаема, нужно только знать, кто что вызывает. Компилятор граф вызовов строить может.

                        Напомню, вопрос не в том, вызовется NeverCalled() или нет — вопрос в том, вызовется ли она до вызова Do().
                        Удаление мёртвого кода тут вообще никаким боком.


        1. 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 рулез.


          1. khim
            27.09.2017 23:45
            +2

            С точки зрения спецификации, ничего не изменилось — как был UB, так UB и остался (ибо спецификация ничего не говорит об SIGSEGV). Зато фактическое поведение перестало вызывать WTF и диагностируется вполне штатными механизмами. В общем, principle of least astonishment рулез.
            А вот принципы «10x тормоза» — это не рулез ни разу.

            Описанная тут оптимизация позволяте заинлайнить функции, что позволяет проделать кучу других оптимизаций. Примеры кода, где это даёт заметное примущество — вполне не академические.


          1. lorc
            28.09.2017 01:49
            +1

            В момент компиляции компилятор уже вполне в курсе target-платформы и уже имеет право знать как на ней обрабатываются вызовы функций по nullptr.

            Извините, но нет. В момент компиляции ничего не знает про рантайм и настройки процессора. Я вполне могу замапить по нулевому адресу страничку памяти и у меня не будет никаких SIGSEGV. Мой рантайм (например bare metal прошивка) может ничего не знать про SIGSEGV.
            Вы не забывайте что C — это не только x86. И что его рантайм — это не только «богатая» ОС.


        1. slonopotamus
          27.09.2017 23:34

          [deleted]


  1. laphroaig
    27.09.2017 17:15
    +10

    Вот из-за таких вот сюрпризов у меня «непонятно почему правильно работающая программа» вызывает на порядок больше паники, чем «непонятно почему неправильно работающая программа»


  1. Resert
    27.09.2017 17:20
    +5

    Ждём когда юный Джуниор соберет это и запустит на проде от рута!


    1. bfDeveloper
      27.09.2017 17:38

      Кстати да, очень жёсткий пример для публичного кода. Можно было ограничиться prinf(«FAIL»); Я вот сразу пошёл компилить и проверять. Хорошо хоть clang'ом скомпиленное не запускал. Из-под рута не сижу, но приятного мало и для юзера.


    1. AllexIn
      27.09.2017 18:07
      +1

      А чем выполнение под рутом лучше выполнения под юзером?
      Насколько я понимаю — снесется всё до чего дотянется rm, в том числе и home/Мне вот ОС не ценна. Переставлю за час.
      А вот потеря пользовательских данных — уже более критична. Хоть большая часть и бэкапится — разгребать последствия придется всё равно…


    1. MacIn
      27.09.2017 18:17
      +1

      Да без проблем. Нет в Windows rm -f


    1. Vadem
      27.09.2017 18:23
      +2

      Вроде как сейчас в большинстве дистрибутивов эта команда не выполнится.
      Нужно писать rm -rf --no-preserve-root /
      Проверять это я конечно не буду :)


      1. BlessMaster
        27.09.2017 18:40
        +1

        Для этого придумали виртуалки )


        1. Vadem
          27.09.2017 20:46

          Да. Или контейнеры. Но кажется, что достаточно старого дорого chroot'а.


      1. vlx
        27.09.2017 19:04

        Rm -rf /*


  1. easty
    27.09.2017 17:33
    +5

    Надо ремарочку поставить. Все трюки выполены професионалами, не потвторяйте это дома. )))


    1. Tufed
      28.09.2017 11:20

      на работе можно )))


  1. michael_vostrikov
    27.09.2017 17:47

    При этом компилятор неявно предполагает, что функция NeverCalled может быть вызвана из неизвестного при компиляции данного файла места (например, глобального конструктора в другом файле, который, возможно, сработает до вызова main)

    А почему он не предполагает, что там же может присваиваться другое значение переменной Do?


    1. technic93
      27.09.2017 17:57
      +8

      Do объявлен static


      1. michael_vostrikov
        27.09.2017 20:30

        А, ну да, в статье же написано. По незнанию не полностью понял смысл той части)


    1. DCNick3
      27.09.2017 21:34
      +3

      В С ключевое слово static гарантирует использование переменной/функции только в том модуле, в котором она объялена (она не экспортируется).


  1. homm
    27.09.2017 18:14
    +4

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

    Вот тут явная логическая ошибка. Никто не «допускает» в своей программе неопределённое поведение. Программисты пишут код, программист никогда не пишет «неопределенное поведение начинается здесь». В этом коде может быть, а может не быть неопределенного поведения. Код, содержащий неопределенное поведение ничем не отличается от кода, не содержащего его.


    Кто-то конечно может считать, что «настоящий программист» должен помнить наизусть всю спецификацию языка, с которым работает. Проблема в том, что эти 15 человек на планете (попадающие под определение «настоящего программиста») физически не смогут написать весь код, который необходим всему остальному человечеству.


    Конечно, игра «напиши еще одну строчку кода и не отстрели себе голову» — очень увлекательная логическая головоломка. Но я хоть убей не понимаю, как ей пользоваться для написания настоящих приложений в 2017 году.


    1. ozkriff
      27.09.2017 18:16
      +2

      Эм. Я уверен это было в значении "допустил ошибку", только "ошибка" конкретная — UB.


      1. MacIn
        27.09.2017 18:18
        +1

        О том и речь:

        Конечно, игра «напиши еще одну строчку кода и не отстрели себе голову» — очень увлекательная логическая головоломка. Но я хоть убей не понимаю, как ей пользоваться для написания настоящих приложений в 2017 году.


        1. ozkriff
          27.09.2017 18:32

          А, ну если это было не в смысле "компилятор виноват", а "язык язык", то я целиком согласен и выход простой — использовать другие языки, где меньше UB. Или высокоуровневые (когда задача позволяет), или более хитрые низкоуровневые ;) .


    1. khim
      27.09.2017 18:35
      +4

      Код, содержащий неопределенное поведение ничем не отличается от кода с ошибкой, не содержащего его.
      Так правильнее будет, не находите?

      Компилятор не заметит и не исправит ошибку на единицу, не заменит неправильное "/2" на "*2" (из-за которого, скажем, будет просматриваться только четверть массива вместо полного) и так далее.

      Почему вдруг компилятор должен вот конкретно вот эти вот ошибки обратывать и «обслуживать» особо? Программист совершил ошибку — он же её и исправит (после восстановления данных из backup'а).


      1. MacIn
        27.09.2017 18:38
        -3

        Почему вдруг компилятор должен вот конкретно вот эти вот ошибки обратывать и «обслуживать» особо? Программист совершил ошибку — он же её и исправит (после восстановления данных из backup'а).

        Потому что здесь нет ошибки программиста.


        1. ozkriff
          27.09.2017 18:48
          +4

          А чья же? Программист взялся писать код на языке, но не смог соблюсти все инварианты.


          Можно сказать что отчасти это ошибка авторов языка, отчасти может быть ошибка разработчиков компилятора, отчасти ошибка выбиравшего язык для проекта, отчасти может быть ошибка настройщика статического анализатора и всякой такой фигни… Но привел-то все это в действие кривым кодом именно программист, так что он явно хотя бы отчасти виноват.


        1. 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;
          }


          Нет, не ндравится? Не хотите заставить компилятор добиваться, чтобы такие программы работали? А в чём отличия с примером из статьи?


          1. michael_vostrikov
            27.09.2017 21:02
            +1

            Отличие в том, что в примере из статьи программистом не указан вызов функции, а в ваших примерах указано обращение к массиву.


            1. khim
              27.09.2017 21:40
              +2

              Ну то есть вы всё-таки предлагаем делить UB на «хорошие» и «плохие». Ok.

              Что будем с подобным примером делать? Оптимизировать или мириться с тем, что какой-нибудь ICC будет наш компилятор рвать «как тузик грелку» в некоторых тестах и некоторых программах?


              1. michael_vostrikov
                28.09.2017 09:17
                -2

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

                Как решать я уже написал. Нужно явно обозначать, что функция вызывается извне. Тогда в статье обозначения не будет (и будет честный вызов NULL), а в примере с движком будет (и будет оптимизация).


                1. mayorovp
                  28.09.2017 09:25
                  +1

                  А чем вам не устраивает подход "явно обозначать, что функция не вызывается извне"?


                  1. michael_vostrikov
                    28.09.2017 09:35

                    Тем, что он приводит к ситуации из статьи. Раз компилятор не видит всю программу целиком, значит надо сообщать ему о поведении других частей, а не разрешать ему додумывать. Тем более что проблема больше связана с инициализацией, и модули бы решили эту проблему. Вызывается в конструкторе (который вызывается извне)? Значит такая оптимизация корректна.


                    1. mayorovp
                      28.09.2017 09:40
                      +2

                      В данный момент существует способ сообщить компилятору информацию о поведении других частей.


                      Более того, если и правда сообщить компилятору что NeverCalled снаружи тоже не вызывается — эффект пропадает


        1. AllexIn
          27.09.2017 18:57

          Вызов метода по заведомо нулевому указателю — не ошибка??
          Нет, я, конечно, последователь метода failfast, но даже он не предполагает, что мы будем ронять приложение заведомо кривыми вызовами…


          1. homm
            27.09.2017 19:00

            Вызов метода по заведомо нулевому указателю — не ошибка??

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


            1. khim
              27.09.2017 19:13
              +3

              А вдруг нет, если именно такой была цель — вызвать исключение доступа?
              То у вас случился epic fail, если вы исполняете программу на ARM без MMU. Там по адресу 0 живёт процедура инициализации, которая перезапускает всю систему. Не совсем rm -rf — но почти. И без всякой «помощи» со стороны компилятора…


              1. homm
                27.09.2017 19:16

                Ну вот видите, вы даже более полезное применение нашли такому поведению. В чем же тут тогда epic fail?


                1. khim
                  27.09.2017 19:27
                  +5

                  В чем же тут тогда epic fail?
                  В том, что ожидаемого поведения (вызвать исключение доступа) вам получить не удалось.

                  А стало быть в переносимой программе его быть не должно. Я уже писал.

                  Принцип такой: если ваша программа не будет работать на процессорах Cray (где, внезапно, nullptr не состоит из одном нулевых битов), если она не будет ботать на системе с дополнением до одного, если она не сможет работать на процессоре без MMU (где вызов кода по адресу nullptr «уничтожает» вашу программу) и т.д. и т.п. — то эта программа ошибочна.

                  Разработчиков компилятора не волнуют ваши отговорки типа «ну мой же код будет работать тольго под Windows и только под 32-бит!» — если вы хотите писать только и исключительно под Windows и только под 32-бит, то вам нужно выбрать язык и компилятор, которые ничего другого не собираются поддерживать никогда.


                  1. salas
                    27.09.2017 20:56
                    +1

                    если вы хотите писать только и исключительно под Windows и только под 32-бит, то вам нужно выбрать язык и компилятор, которые ничего другого не собираются поддерживать никогда.

                    А допустим, вот прямо так. Только Windows и только 32 бита, желаемый уровень абстракции — ну, в общем, такой, на котором можно вызывать WinAPI без обёрток. Какие языки и компиляторы Вы бы посоветовали?


                    1. ozkriff
                      27.09.2017 20:58
                      +3

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


                      1. salas
                        27.09.2017 22:03

                        Я в целом догадываюсь, что именно такова суровая исторически сложившаяся реальность за пределами тёплой и сухой области применимости условных питона или джавы — или Cray без MMU, или конкретные версии компиляторов, и никаких компромиссов. Но, может быть, что-то пропускаю? Процитированная рекомендация подразумевает что-то третье ("никогда" — это, кажется, не про конкретную версию).


                  1. slonopotamus
                    27.09.2017 23:17

                    Пока мы говорим про программу в вакууме — да, вы правы. Но как только в дело вступает компилятор, у нас появляется знание о целевой платформе и её свойствах. Поэтому на этапе компиляции часть UB превращается во вполне себе defined behavior.


                    1. khim
                      27.09.2017 23:52
                      +3

                      Поэтому на этапе компиляции часть UB превращается во вполне себе defined behavior.
                      Только в том случае если вы это запросили явно. Хотите чтобы переполнение у целых числе не считалось UB? Задайте -fwrapv. Хотите использовать каламбур типизации? -fno-strict-aliasing — и нет проблем.

                      Но по умолчанию — нет. Программа должна работать на CRAY MP с дополнением до одного — и точка.


      1. homm
        27.09.2017 18:57
        +6

        Так правильнее будет, не находите?

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


        не заменит неправильное "/2" на "*2"

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


        1. khim
          27.09.2017 19:19
          +1

          В Си, насколько я понимаю, даже порядок вычисления аргументов не определен.
          Не полностью определён. Но описаний того, что произойдет достаточно для того, чтобы писать программы.

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

          То, что компилятор (стандарт языка) не может магическим образом исправить неправильный код, по-вашему каким-то образом дает ему карт-бланш ломать правильный код?
          Любой код, вызывающий UB — неправильный. По определению. Как я уже писал программа, вызвающая UB может исполнять что угодно и когда угодно — причём в стандарте отдельно подчёркнуто, что «когда угодно» — распространяется, в том числе, на время до точки, когда UB произойдёт.


          1. homm
            27.09.2017 19:23
            -6

            Любой код, вызывающий UB — неправильный.

            Еще раз — почти любой код на Си содержит UB, как минимум из-за прядка вычисления аргументов. Получается «Почти любой код, написанный на Си — неправильный». Так зачем пользоваться таим языком?


            1. khim
              27.09.2017 19:35
              +9

              Еще раз — почти любой код на Си содержит UB, как минимум из-за прядка вычисления аргументов.
              Порядок вычисления аргументов — это UNSPECIFIED behavior, ни в коем разе не undefined.

              Так зачем пользоваться таим языком?
              Человеку, который путается в терминах и не понимает, что unspecified, undefined и implementation defined — это всё разные вещи с разными последствиями действительно стоит использовать какой-нибудь другой язык, попроще.


  1. Vindicar
    27.09.2017 19:16
    +1

    Я, конечно, не профи, но все же…
    По-моему тут нарушается принцип наименьшего удивления.
    Падение программы в GPF — меньший (и менее неприятный) сюрприз по сравнению с вызовом не той функции. Проще отловить, проще понять, проще исправить.


    1. khim
      27.09.2017 20:40
      +3

      Эта программа — ошибочна. Проблемы возникают только когда вы вызываете UB. Точка. Конец дискуссии. Как я писал выше — эту программу таки можно использовать так, чтобы не вызывать UB. И вот в этом случае — она будет работать корректно.

      А вот как она будет работать если вы вызываете UB — разработчиков компилятора не волнует. Как-то работает — и ладно.


  1. potan
    27.09.2017 20:45

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


    1. khim
      27.09.2017 21:08
      +1

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

      Добавьте функцию, которая позволить вам на эту переменную посмотреть из другого модуля — и функция NeverCalled станет непустой и переменна Do вернётся. Но из main по прежнему будет вызываться rm -rf, так как ничего другого без попадания на UB вы сделать не сможете.


  1. Zakyann
    27.09.2017 20:47
    -6

    Как это по-хабовски — минусовать за собственное мнение. Смешно смотреть, как 90% боятся высказаться против 'тренда' из-за боязни слить карму. Но мне пофиг, я не кармадрочер. Аккаунт сгниёт — еще один заведу :) Минусуйте на здоровье :)

    Зачем мне думать за компилятор? Всё, что максимально можно сгрузить на компьютер, должно быть на него сгружено.
    Есть возможность максимально проверить типы? Делаем. Есть возможность однозначно выполнять код — делаем. Есть возможность максимально уйти от ошибок — делаем.

    Падение программы в GPF — меньший (и менее неприятный) сюрприз по сравнению с вызовом не той функции

    Это, конечно, вообще ад. В делфи-паскале такое представить сложно. Разве что программер сам накосячит и указатель не на ту функцию передаст. Но это уже его косяк, а не 'особенности' реализации.


    1. 0xd34df00d
      27.09.2017 21:29
      +2

      Есть возможность максимально проверить типы? Делаем.

      Просто странно приводить паскаль в пример такого языка.


      1. Zakyann
        27.09.2017 21:56

        А что с ним не так? Есть какие-то проблемы с проверкой типов?


        1. 0xd34df00d
          27.09.2017 22:08
          +2

          Далеко не самый строгий язык с далеко не самой выразительной системой типов.


          1. Zakyann
            27.09.2017 22:33

            Лучше всего, когда слова подкрепляются примерами.


            1. 0xd34df00d
              27.09.2017 22:34
              +2

              Rust, Haskell, Clean, Idris. Да мало ли.


              1. Zakyann
                28.09.2017 04:56
                -1

                Знаете, сколько я еще интересных названий языков знаю? :) Сравнения какие-то будут? Покажете более выразительную систему типов или более строгую типизацию?


                1. 0xd34df00d
                  28.09.2017 06:23
                  +2

                  Взгляните на таковые в этих языках, например.


                  1. Zakyann
                    28.09.2017 17:21
                    -4

                    Понятно. Вопросов больше не имею :)


                    1. 0xd34df00d
                      28.09.2017 20:56
                      +2

                      Я не понимаю, какого ответа вы ожидали. Сравнения по пунктам различных систем типов? Рассказа, что в паскале можно из того, что в тех языках нельзя?


    1. splav_asv
      27.09.2017 21:57
      +1

      Вашим воззрениям на язык, язык C просто не удовлетворяет. Чем уверять, что язык плохой, просто пользуйтесь другими. У языка C и языка C++ пока ещё есть уникальные свойства, благодаря которым во многих областях их заменить ничем особо не получается.
      Со временем, конечно, языки постепенно стремятся к вашему представлению, но это отнюдь не просто.


    1. mayorovp
      28.09.2017 09:10

      У вас есть какие-то пруфы относительно того что 90% боятся высказывать свое мнение?


  1. RolexStrider
    27.09.2017 21:51

    typedef int (*Function)();

    Уже одна эта строчка сразу навела на мысль, что дальше по тексту будет про UB и прочая чёрная магия. Так и оказалось.


    1. johnnymmc
      27.09.2017 21:58
      -1

      Интересно, кто-то в здравом уме вообще может написать такое в реальном, не «эзотерическом» проекте?


      1. tangro Автор
        28.09.2017 10:44
        +7

        Какое «такое»? Указатель на функцию тайпдефнуть? В каждом первом реальном проекте на С/С++ такое есть.


        1. DmitryMe
          28.09.2017 11:23
          +1

          Практический пример — вызов функций WinAPI, доступных только в части версий Windows, на которых должна работать программа. Указатель на функцию получается вызовом GetProcAddress(), его нужно потом привести к типу «указатель на фнукцию с такими-то параметрами и таким-то возвращаемым значением».


    1. ozkriff
      27.09.2017 22:11
      +4

      Вроде ж просто сишный псевдоним для указателя на функцию. Я наоборот видел мало кода, где с указтелями на функции работают без typedef (спасибо "уникальному" синтаксису объявлений си).


      Или дело просто в том что есть указатель на функцию?


  1. johnnymmc
    27.09.2017 21:56

    А не логичнее ли компилятору (или вообще препроцессору) вообще выкинуть неиспользуемую функцию?


    1. khim
      27.09.2017 21:58
      +4

      А как он узнает, что она не используется? Её могут из другого модуля вызывать.


      1. DistortNeo
        27.09.2017 22:01

        Не могут. Она же static.


        1. khim
          27.09.2017 22:10
          +1

          Так все static от извёл. Он NeverCalled извести не может — но может сделать пустой. Что и было проделано.


  1. ozkriff
    27.09.2017 22:55

    Кстати, если кому интересно, на тему UB есть очень доходчивый доклад:


    https://www.youtube.com/watch?v=dM2BfeppYcM


  1. MrShoor
    28.09.2017 01:29

    Проанализировать исходник аж всего приложения, и увидеть, что в Do значение присваивается лишь однажды — у компилятора мозгов хватило. А элементарно то, что функция, в которой есть это самое присвоение никогда не вызывается — нет? Что за странный оптимизатор?


    1. lorc
      28.09.2017 01:58
      +4

      А элементарно то, что функция, в которой есть это самое присвоение никогда не вызывается — нет?

      Она может вызваться из другой единицы трансляции. Компилятор — не линковщик, всю программу целиком не видит.


      1. MrShoor
        28.09.2017 04:16

        Да, действительно, спасибо. Ведь Do объявлена как static, и поэтому достаточно проанализировать только этот файл. А я ошибочно подумал, что компилятор проанализировал всю программу.


  1. khim
    28.09.2017 04:17
    +1

    Интересно, кстати, как иногда баяны всплывают. Я только сейчас осознал, что шумиху вокруг этого примера породил я, упомянув его в внутренней рассылке — а кто-то вытащил его на redddit и пошло-поехало.

    Так-то оригинал тут — и опубликовали его… давно, в общем.


  1. Nick_Shl
    28.09.2017 04:28

    Добавить к "static Function Do;" volatile и 99% даю, что будет вызываться NULL.


  1. 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 там, где может выполняться мусор.


    1. 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.
      Позиция разработчиков компиляторов проста и незатейлива: хотим чего-то подобного — пишем пропозал — получаем результат.

      Просто потому что ссылки на здравый смысл не работают. Он у всех разный, как показывает этот тред…


      1. gul_kiev
        28.09.2017 20:16

        > Если к unsigned, то он прав. И формально и фактически.

        О, вот хороший пример. Я знаю, как оно на низком уровне, и знаю, что integer overflow — это UB. Поэтому не удивлюсь, если и для unsigned int переполнение окажется UB. По стандарту не оказалось, фух. Но если для signed переполнение UB — это всё равно минное поле.


    1. 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.


  1. Qualab
    28.09.2017 15:21
    -7

    Давайте честно скажем, что в шланге багло, несмотря на Undefined behavior, компилятор берёт на себе излишне много.


    1. khim
      28.09.2017 16:58
      +3

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

      Дело в том, что в данном случае «стреляет» не экзотическая оптимизация, которая «срабатывает» на каком-то синтетическом бенчмарке, а вполне себе реальная, которая позволяет оптимизировать реальные программы. Идею кода, где это бывает я обрисовывал.

      Любовь программистов, выросших на Java, устраивать 100500 индирекций делает подобные оптимизации весьма полезными, так что ломать их ради программы с ошибкой — никто не будет.


      1. netch80
        29.09.2017 12:12

        > В точности нуль разработчиков придерживаются этой точки зрения. То есть ни одного «сочувствующего» вы не найдёте.

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


        1. khim
          29.09.2017 13:08

          Разработчиков компиляторов
          Разработчиков компиляторов или людей, хотя бы способных послать осмысленное предложение по изменению оного компилятора.

          или целевых программ — их пользователей?
          Как любит говорит Линус: Talk is cheap. Show me the code.

          Совершенно непонятно откуда возьмётся компилятор, что-либо делающий по-другому, если его некому разрабатывать. Ну вот совсем некому. Даже хуже — никто из людей, громко тут «бурлящих» не имеет ни малейшего представления о том, что у него «под капотом» и как оно там работает! Вот, Mingun договорился до того, что у него линкер и компилятор стали «одним исполняемым файлом», что явно показывает уровень понимания, согласитесь?


          1. ozkriff
            29.09.2017 13:18
            +1

            Я думаю что netch80 не спорил с неадекватностью этой точки зрения, а как раз говорил что среди прикладных программистов большинство толком не понимают как вообще работает компилятор, так что "ни одного «сочувствующего» вы не найдёте." — не верно, потому что их полно (и это печально).


            1. netch80
              30.09.2017 09:39
              +2

              И да, и нет. Безусловно, большинство прикладных программистов не понимают, как работает компилятор; но им обычно это и не нужно, нужно иметь положительную часть опыта (как делать) и отрицательную (как не делать, где грабли). Но основное таки не в работе компилятора, а в том, когда он становится усилителем ошибок. Пример в исходном постинге темы не настолько характерен, как, например, этот; см. по тексту, как или изменение опций компиляции, или небольшая правка исходника, не меняющая суть выполняемого, сменяет неограниченный цикл на ограничение 3 итерациями в цикле. Вот это случай, когда возможность сделать UdB откровенно абьюзится авторами компилятора, а программисту найти такое, если кода много и/или оно закопано в макросах, может быть очень тяжело.

              Ну а поскольку безошибочных программ вообще не бывает (helloworld?ы не считаем, и то неизвестно, что там в libc) — совершенно очевидно и обоснованно создаётся ощущение минного поля, авторы которого тут же с краю стоят и усмехаются — «ну-тко, кто ещё на что нарвётся?» А с соседнего поля кричат «а у нас мин нет, а ещё есть печеньки (вариант: батарейки)»…


              1. tyomitch
                01.10.2017 02:00
                +2

                Я бы проводил аналогию не с минным полем, а со слесарной мастерской, уставленной мощными станками: если неаккуратно пользоваться, оставит без рук. А из соседней мастерской кричат «а у нас все инструменты надувные, даже не ушибёшься!»


                1. netch80
                  01.10.2017 07:45

                  В «соседней мастерской» не надувные, а реальные и делающие ровно то, что им задают. А в мастерской C — такие, что если на них хоть чуть-чуть ошибся, они через полчаса (когда ты давно завершил кусок работы и занялся другим) выстрелят, совершенно законно, тебе сверлом в спину.


          1. 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 — то насколько это важно по сравнению с основной массой? Не лучше ли создать профиль, покрывающий практически всех?

            > никто из людей, громко тут «бурлящих» не имеет ни малейшего представления о том, что у него «под капотом» и как оно там работает!

            Ну я понимаю (не на уровне активного разработчика компилятора, но на достаточном, чтобы знать, что он делает и почему; и собственный опыт на мелкие поделки, включая кодогенерацию). И это не останавливает меня от вопроса, зачем нужен язык, который способен настолько усиливать неизбежные ошибки программиста…

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


            1. 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++ — в любом случае относится к категории сложных и небезопасных языков), но при этом потеряют тех, кому важна скорость.


              1. michael_vostrikov
                30.09.2017 05:20
                -3

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

                Зачем, если проверки и соответствующее поведение и так находятся в процессоре?


                1. 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, если он больше, и дальше используется уже машинная команда.


                  1. khim
                    30.09.2017 17:35
                    +1

                    они игнорируют старшие биты
                    Если бы они все хотя бы игнорировали биты — это было бы полбеды. А так — x86 игнорирует все «лишние» биты и сдвиг 32-битного числа на 33 — это то же самое, что сдвиг на 1. А ARM — игнорирует, но не все: сдвиг на 33 — отрабатывается правильно, а вот сдвиг на 257 — это опять то же самое, что и сдвиг на 1.

                    Собственно поэтому это и UB в C/C++.

                    А если кому-то нужно гарантировать оптимизированность операции — например, транслятор не может понять, что сдвиг будет в нужных пределах — предоставить какие-нибудь int::native_sll(arg, shiftcount), который будет builtin?ом транслятора.
                    Если вы делаете свой, новый, язык «с иголочки» — то вы можете себе это позволить. Но если язык у нас уже есть и на нём написаны миллиарды строк кода — то глупо как-то его брать и замедлять на ровном месте…


              1. 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 существует уже много лет, чем эти опции хуже?


                1. 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)?

                  Но формально — да, вы правы, признаю, был неточен…
                  А ещё Вы говорили рядом:

                  >> Например 32-битная единица, сдвинутая вправо на 33 бита на x86 даст 2, а на ARM'е — 0.

                  Проясните? Это тот же случай, или это ARM64 со сдвигами только двойными словами?
                  Тот же, но не совсем. То есть для чисел до 255 всё работает как в Go. Однако. В ARM2 в barrel-shifter подавался только младший байт операнда. Во всех процессорах (включая самые последние) сдвиг работает так же. Совместимость-с.

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

                  Да. Только этого ли хотели добиться, ужесточая наказания за ошибку?
                  Нет, конечно. Никто никого «ужесточать» не пытается. Пытаются сделать более быстрый компилятор. Достаточно успешно: 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, и безотносительно общего уровня оптимизации.
                  Извините за почти бесплодные мечтания (по поводу C).
                  Почему бесплодные? Они бесплодны пока вы тут в комментариях «срётесь». Оформите в виде формального «proposal»а — и у вас есть шанс.

                  Разработчики компиляторов — они не садисты, у них нет цели кого-либо наказать. Просто когда в 99.999% случаев всё и так работает без всяких «лишних» проверок (как в примере со сдвигами выше), то глупо на них тратить время — особенно если стандарт этого не требует.

                  И спасибо за признание, что UdB для знакового переполнения — это именно рудимент от времён динозавров.
                  Ну дык никто и не спорит. Знаковое переполнение — это рудимент от старых архитектур, правила алиасинга — это от времён, когда сопроцессор, работающий с правучкой — был физически отдельным процессором. И так далее.

                  Но проблема в том, что даже если сейчас процессоры стали работать по-другому — это уже ничего не меняет: существующие программы на UB не полагаются (во всяком случае не должны полагаться) — так почему бы это не использовать для оптимизации?

                  Пока что всё это не отменяет того вывода, что сишники своими руками прогоняют тех, кто при относительно небольших усилиях авторов трансляторов мог бы остаться их пользователем.
                  Я вот в этом совсем не уверен. Ибо они гонял-гонят, а прогнать никак не могут. На TIOBE C и C++ — по прежнему языки #2/#3, а Go и D — где-то во втором-третьем десятке. Правда относительная популярность падает — но она и у Java падает так же стремительно, так что сдаётся мне, что не в UB дело.

                  Тот же -fwrapv существует уже много лет, чем эти опции хуже?
                  Если они всего лишь «не хуже», то смысла в них особого нет. Сейчас проверил: в исходниках Android'а -fwrapv используется в 5 проектах: syslinux, dEQP, Python и mksh. Ни один из них не является критичным и при необходимости от них от всех можно отказаться.

                  Стоит ли огород городить ради опций, которые всё равно никто не будет использовать?


          1. tyomitch
            01.10.2017 01:38
            +1

            Совершенно непонятно откуда возьмётся компилятор, что-либо делающий по-другому, если его некому разрабатывать. Ну вот совсем некому.

            Переформулирую то же самое с другого угла зрения: компиляторы разрабатываются вполне прагматичными людьми для вполне практических целей. Тот, кто добавлял в Clang оптимизацию, ставшую темой поста — или любую другую оптимизацию — уж точно не потирал ладони: «уж теперь-то вы у меня попляшете, жалкие людишки, допускающие в своём коде UB!» Никто не станет реализовывать абсурдные трансформации кода лишь затем, чтобы досадить программистам — независимо от того, допускает стандарт такие трансформации или нет.
            Наоборот: если какая-то оптимизация была реализована и продолжает поддерживаться, это значит, что в реально компилируемом коде она полезна, даже если в отдельных «лабораторных» примерах она кажется абсурдной.


  1. Zakyann
    28.09.2017 18:22
    -10

    Всем кармадрочерам: прощайте, до следующей моей инкарнации

    image