Пользователь
11,5
рейтинг
16 июля 2014 в 03:48

Разработка → Неопределённое поведение и теорема Ферма

В соответствии со стандартами C и C++, если выполнение программы приводит к переполнению знаковой целой переменной, или к любому из сотен других «неопределённых действий» (undefined behaviour, UB), то результат выполнения программы может быть любым: она может запостить на Твиттер непристойности, может отформатировать вам диск…
Увы, в действительности «пасхальные яйца», которые бы заставляли программу в случае UB делать что-то из ряда вон выходящее, не встречались со времён GCC 1.17 — та запускала nethack, когда встречала в коде программы неизвестные #pragma. Обычно же результат UB намного скучнее: компилятор просто оптимизирует код для тех случаев, когда UB не происходит, не придавая ни малейшего значения тому, что этот код будет делать в случае UB — ведь стандарт разрешает сделать в этом случае что угодно!
В качестве иллюстрации того, как изобилие UB в стандарте позволяет компилятору выполнять неочевидные оптимизации, Реймонд Чен приводит такой пример кода:

int table[4];
bool exists_in_table(int v)
{
    for (int i = 0; i <= 4; i++) {
        if (table[i] == v) return true;
    }
    return false;
}

В условии цикла мы ошиблись на единицу, поставив <= вместо <. В итоге exists_in_table() либо должна вернуть true на одной из первых четырёх итераций, либо она прочтёт table[4], что является UB, и в этом случае exists_in_table() может сделать всё что угодно — в том числе, вернуть true! В полном соответствии со стандартом, компилятор может соптимизировать код exists_in_table() до
int table[4];
bool exists_in_table(int v)
{
    return true;
}

Такие оптимизации иногда застают программистов врасплох. Джон Регер приводит подборку примеров, когда UB приводило к значительным последствиям:
  • UB при знаковом сдвиге влево позволило компилятору удалить из NaCl проверку адреса возврата, важную для безопасности.
  • В ядре Linux, разыменование указателя перед его проверкой на NULL позволило компилятору удалить эту проверку, создав уязвимость в системе.
  • В Debian, использование неинициализированного массива в качестве источника случайных данных для инициализации RNG seed привело к тому, что всё вычисление seed было удалено компилятором.
  • Когда переменная p не инициализирована, программа может выполнить и код внутри if (p) { ... }, и код внутри if (!p) { ... }.
  • Когда знаковая переменная x равна INT_MAX, выражение (x+1)>x в разных местах одной программы может трактоваться и как истинное, и как ложное.
  • Бесконечный цикл, например поиск несуществующего значения, может быть удалён компилятором. Например, так компилятор может «опровергнуть» Великую теорему Ферма. (Этот пример мы ещё разберём подробно.)
  • Компилятор может сделать программу «ясновидящей», переставив операцию, потенциально способную обрушить процесс (деление на ноль, чтение по нулевому указателю и т.п.), вперёд операции вывода. Например, вот этот код:
    int a;
    
    void bar (void)
    {
      setlinebuf(stdout);
      printf ("hello!\n");
    }
    
    void foo3 (unsigned y, unsigned z)
    {
      bar();
      a = y%z;
    }
    
    int main (void)
    {
      foo3(1,0);
      return 0;
    }
    

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


В 2012 Регер объявил конкурс на «самый причудливый результат UB». Один из победителей воспользовался тем, что использование указателя после того, как он передан параметром в realloc(), является UB. Его программа печатает разные значения по одному и тому же указателю:
#include <stdio.h>
#include <stdlib.h>
 
int main() {
  int *p = (int*)malloc(sizeof(int));
  int *q = (int*)realloc(p, sizeof(int));
  *p = 1;
  *q = 2;
  if (p == q)
    printf("%d %d\n", *p, *q);
}

$ clang -O realloc.c ; ./a.out 
1 2


Программы остальных победителей конкурса, на мой взгляд, скучнее и частично перекрываются с ранее приведёнными примерами.
Но ничто не сравнится с примером самого Регера — «опровержением» компилятором теоремы Ферма.

Он объясняет, что для какого-то встроенного приложения ему нужно было написать на C++ бесконечный цикл так, чтобы оптимизирующий компилятор не смог удалить из программы весь код, следующий за циклом. Современные компиляторы достаточно умны, чтобы узнавать «идиомы» навроде while (1) { } или for (;;) { } — они понимают, что код, следующий за таким циклом, никогда не выполнится, а значит, его и компилировать незачем. Например, функцию
  void foo (void)
  {
    for (;;) { }
    open_pod_bay_doors();
  }

— большинство компиляторов «укоротят» до единственной инструкции:
  foo:
    L2: jmp L2

Clang оказывается ещё умнее, он способен распознавать (и удалять) даже такие замаскированные бесконечные циклы:
  unsigned int i = 0;
  do {
    i+=2;
  } while (0==(i&1));

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

Регер решил: уж теорему Ферма-то компиляторы вряд ли смогут доказать во время компиляции?

int fermat (void)
{
  const int MAX = 1000;
  int a=1,b=1,c=1;
  while (1) {
    if (((a*a*a) == ((b*b*b)+(c*c*c)))) return 1;
    a++;
    if (a>MAX) {
      a=1;
      b++;
    }
    if (b>MAX) {
      b=1;
      c++;
    }      
    if (c>MAX) {
      c=1;
    }
  }
  return 0;
}

#include <stdio.h>

int main (void)
{
  if (fermat()) {
    printf ("Fermat's Last Theorem has been disproved.\n");
  } else {
    printf ("Fermat's Last Theorem has not been disproved.\n");
  }
  return 0;
}

regehr@john-home:~$ icc fermat2.c -o fermat2
regehr@john-home:~$ ./fermat2
Fermat's Last Theorem has been disproved.
regehr@john-home:~$ suncc -O fermat2.c -o fermat2
"fermat2.c", line 20: warning: statement not reached
regehr@john-home:~$ ./fermat2
Fermat's Last Theorem has been disproved.


Как так? Цикл завершится по return 1; — компилятор смог доказать, что теорема Ферма неверна?!

Интересно, какие же значения a,b,c он «нашёл»?

Регер добавил печать «найденных значений» перед return 1; — вот тогда компилятор признал бессилие и честно скомпилировал бесконечный цикл. (Ничего, естественно, не напечаталось.)

Несмотря на то, что эта программа не содержит никаких арифметических переполнений (множители изменяются в пределах 1..1000, сумма их кубов не превосходит 231), стандарт C++ объявляет «неопределённым действием» бесконечный цикл без изменения внешнего состояния — поэтому компиляторы C++ вправе считать любой такой цикл конечным.
Компилятор легко видит, что единственный выход из цикла while(1) — это оператор return 1;, а оператор return 0; в конце fermat() недостижим; поэтому он оптимизирует эту функцию до
int fermat (void)
{
  return 1;
}


Иначе говоря, единственная возможность написать на C++ такой бесконечный цикл, который компилятор не смог бы удалить — это добавить внутрь цикла изменение внешнего состояния. Самый простой (и стандартный!) способ сделать это — изменять переменную, объявленную как volatile.
@tyomitch
карма
306,7
рейтинг 11,5
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (125)

  • НЛО прилетело и опубликовало эту надпись здесь
      • НЛО прилетело и опубликовало эту надпись здесь
        • +1
          Соотносится безо всяких проблем: *(table+4) — тоже UB.
          Т.е. спецификация не предписывает, как такой код должен работать — имеет право упасть с SIGSEGV, имеет право отформатировать диск.
          Как предложите действовать по спецификации, которой нет?
          • НЛО прилетело и опубликовало эту надпись здесь
            • +6
              Расписал чуть ниже.
              Если кратко — стандарт не указывает, в какой ассемблерный код транслировать операторы C++.
              Он указывает только результат выполнения — когда его возможно указать. В этом случае — невозможно.
              • НЛО прилетело и опубликовало эту надпись здесь
                • +4
                  1) «contiguously allocated» относится не к реализации, а к наблюдаемому поведению: увеличивая указатель на 1, переходим к следующему элементу. Как Halt указал ниже, компилятор может засунуть весь массив в один векторный регистр, и работать с ним там.

                  2) В середине статьи есть замечательный пример того, насколько «однозначно работают» указатели — когда по одинаковым указателям «оказываются» разные значения.

                  3) Стандарт не привязан ни к какой конкретной железке, его цель — определить однозначное соответствие (программа → результат), которое не будет зависеть ни от компилятора, ни от оборудования, ни от погоды в Небраске.

                  Конкретный компилятор может давать какие-то дополнительные гарантии (например, доисторические компиляторы C «узнавали» в подобных конструкциях адрес регистра конкретной железки, и «дописывали» к объявлению volatile), но для соответствия стандарту ничего такого не требуется.
                  • НЛО прилетело и опубликовало эту надпись здесь
                    • +2
                      Но тогда получается, что практически всё, лежащее в основе существующих в реальности систем — результат случайного стечения обстоятельств.

                      stackoverflow.com/questions/21670459/why-is-fi-1-i-1-undefined-behavior
                      It is undefined behavior because it is not defined what the behavior is.

                      (This deserves emphasis because many programmers think «undefined» means «random», or «unpredictable». It doesn't; it means not defined by the standard. The behavior could be 100% consistent, and still be undefined.)

                      Could it have been defined behavior? Yes. Was it defined? No. Hence, it is «undefined».

                      That said, «undefined» doesn't mean that a compiler will format your hard drive...it means that it could and it would still be a standards-compliant compiler.
                      • НЛО прилетело и опубликовало эту надпись здесь
                        • +1
                          Вы правы в том, что практически всё, лежащее в основе существующих в реальности систем, опирается на нестандартные особенности конкретной платформы, и перестанет работать при апгрейде или смене компилятора, ОС или железа.
                          Хотя на конкретной платформе всё стабильно.
                • 0
                  Вы же компилируете код настольных приложений и код приложений под микроконтроллеры не одним и тем же компилятором? И «настольный» компилятор понимает, что указатель невалиден, и никаких адресов регистров у него быть не может. Ну а компиляторы под микроконтроллеры, в свою очередь, наоборот, ожидают что могут быть разные регистры, и, наверное, не стоит оптимизировать работу с указателями. Стандарт это не регламентирует, он говорит что должно быть в случае если всё хорошо, правильная же работа в случае доступа к «регистрам железки» — тоже undefined behaviour. В стандарте, но не в конкретном компиляторе.
                  • НЛО прилетело и опубликовало эту надпись здесь
                  • 0
                    регистры по разным адресам могут быть не только у микроконтролелров, а у PCI-плат, например.
  • –9
    Имхо, должна быть опция компиляции, отключающая проверку на UB и сохраняющая все прочие оптимизации. Иначе как дальше жить?
    • +10
      Вы просто не понимаете, что такое UB и почему компилятор поступает именно так.

      Любое UB возникает потому, что разработчик компилятора оказывается в ситуации выбора: или позволить программисту выстрелить себе в ногу в 0.001% случаев или контролировать каждый чих, уронив при этом производительность. Исторически сложилось, что компиляторы C идут по первому пути, поскольку системные задачи подразумевают что программист понимает что он делает.

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

      P.S.: Проблемы оптимизации это только часть целого, которую нельзя исключить просто потому что так хочется.
      • +3
        Я с Вами согласен, что почти всегда оптимизация невозможна без модификации (или удаления) кода. Но Ваше утверждение «программист понимает что он делает» вступает с этим в логическое противоречие — если бы программист знал, что он хочет получить именно тот результат, который предлагает компилятор, то он именно так бы и запрограммировал бы, прямо и без двусмысленностей.

        Суть моего утверждения в том, что нужно дать режим оптимизированно скомпилировать программу так, чтобы она вычисляла именно то, что желает вычислить программист. Если в результате оптимизации производительности программа начинает выдавать другой результат, вместо того чтобы выдавать прежний за меньшее время, то, по определению, это уже не просто оптимизация производительности, а вмешательство в функционал программы, чего обычно ни один программист не желает получить, не так ли (иначе почему не запрограммировать новый функционал напрямую)?
        • +5
          Если программа тригеррит UB, то у нее нет никакого определенного выдаваемого результата. Поэтому бессмысленно говорить о том, что она стала выдавать другой результат после оптимизаций.
          • +1
            Если программа содержит UB, то весьма сомнительно, что это сделанно преднамеренно (разве что для таких конкурсов, как в статье). Обычно это результат ошибки программиста. Формально да, он зашел в запрещенную зону, где все гарантии снимаются, но суть в том, что он сделал это не преднамеренно, и разумный компилятор не будет подкладывать программисту свинью, пользуясь каждым удобным случаем, по крайней мере, пока его об этом сильно-сильно-сильно не попросят.
        • –4
          проблема в том что многие оптимизации ухудшиют читаемость кода (типа разматывания циклов), поэтому мы имеем текущую ситуацию
        • +1
          >нужно дать режим оптимизированно скомпилировать программу так, чтобы она вычисляла именно то, что желает вычислить программист.
          shadr1.ru/button/button.html
        • +2
          именно то, что желает вычислить программист
          Компилятор не читает мысли, а программы делают то, что им сказано, а не то, что подразумевалось. Особенно это касается Си, одно из главных правил которого: не мешать программисту делать то, что он хочет. А додумывающие компиляторы, бережно вставляющие проверочки на каждый чих, пусть остаются для безопасных managed языков. Или можно использовать языки, которые лучше подходят для написания «достаточно умных компиляторов», чтобы те были способны запретить UB синтаксически (ценой невозможности кастовать слонов в комоды и писать куда попало в память, даже если очень надо).
        • +3
          Помимо того что уже ответили другие участники дискуссии, добавлю:

          Компилятор не может охватить своим «ментальным взором» всю программу. Процесс компиляции/оптимизации осуществляется серией проходов, каждый из которых некоторым образом преобразовывает граф управления. Подобным образом реализуются peephole оптимизации, LICM, CSE, DCE и много других страшных слов. Каждый проход производит свое преобразование и ему нет дела до того, что находится вне его контекста.

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

          P.S.: Отдельной проблемой оказывается генерация адекватных сообщений об ошибках, поскольку они точно так же зависят от изначальной формы кода.
      • 0
        strict aliasing, кстати, много где отключают. Одна из причин — падение производительности на компиляторах, у которых медленный memcpy со статически известной длиной (и которые strict aliasing все равно не поддерживают).
  • –1
    Зря вы ссылаетесь на какого-то Чена, статьи нужно не просто переводить, а ещё и проверять, мало ли какую чушь автор там напишет. Вывод из первого примера — обратный! В нём компилятор обязан прочитать за концом массива, и честно проверить условие, за содержимое памяти отвечает программист, поэтому неопределённость в этом случае означает, что компилятор не знает, что за память он прочитает, поэтому отключит все оптимизации, ведь оптимизации циклов возможны только при известных данных.
    • +9
      Строго говоря, сама дискуссия на тему того, что должен, а что не должен делать компилятор в таком случае некорректна. Если ситуация обозначена как UB то он хоть гопака сплясать может. Поэтому это и UB. А утверждать подобные вещи в отрыве от конкретного компилятора и ее версии нельзя.

      LLVM может векторизовать цикл, студия может оставить как есть, какой нибудь Watcom C — отключить оптимизации и воткнуть проверку, но это в любом случае уже самодеятельность компиляторов.
      • 0
        На самом деле можно написать
        int table[0];

        В любом случае, в компиляторах типа LLVM/GCC, что поддерживают массивы нулевой длины, код будет гарантированно работать как написано без всякой самодеятельности.
        • +3
          Нифига. Обращаться к такому массиву всё равно нельзя. Места, где можно безопасно использовать массивы нулевой длины описаны в документации на компилятор. При простом размещении их на стеке к их элементам обращаться нельзя.
        • 0
          FTGJ, flexible arrays официально в языке начиная ещё с C99:
          struct some_struct
          {
              // ...
              type flexible_array[];
          };
          
          • 0
            Эээ… В каком-таком языке? Вы, надеюсь, понимаете, что C++14 не является надмножеством даже C99 (не говоря уже о C11)?
    • +5
      Halt всё написал правильно, но попробую объяснить ещё подробнее.

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

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

      Я вас уверяю, если бы компиляторы работали так, как предлагаете вы — «любое подозрение на UB отключает все оптимизации» — то ни одна программа сложнее Hello World не была бы оптимизирована.
      • +2
        Я не понимаю одну вещь. Первые 4 итерации цикла корректны, только пятая i==4 вызывает UB. Быть может, что при реальной работе программы i никогда и не дойдёт до 4. Тогда UB быть не должно, но это можно отследить только на этапе выполнения. Так какое имеет компилятор право предполагать, что UB возможно?
        • +9
          Ну, похоже, что компилятор видит, что этот цикл либо закончится с UB, либо return true, т.к. UB он имеет право заменить чем угодно — он пробует заменить return true, получает, что этот цикл всегда return true, радуется, оставляет только эту инструкцию.
          • +3
            Чёрт. Это гениально :)
          • 0
            Только радоваться тут нечему (если компилятор не любитель злостно напакостить). Если бы программист хотел написать return true вместо цикла — он бы так и написал. Такое место нужно не оптимизировать, а обвешать красными флажками.
            • +2
              А вот это — уже идеология современного Си. Не нравится — не ешьте…
            • +1
              Если бы программист хотел написать return true вместо цикла — он бы так и написал.
              Вы так говорите, как будто никогда не смотрели внутрь Boost'а или libstcd++…

              Конечно компилятор «радуется»: вы же ему своими руками указали на случаи, которые в принципе не могут исполнится во время работы программы. Никогда и на за что (правильная программа на C++ никаких UB вызывать не должна). А раз так — у него возникает отличная возможность посмотреть на все последовательности действий, которые могут привести к этому коду — и выкинуть их за ненадобностью.

              Грубо говоря написав if (!this) { blah-blah-blah; } вы сказали компилятору, что blah-blah-blah вы никогда вызывать не будете, о потом удивляетесь: куда пропали все проверки?
        • +2
          Если UB не происходит, то исходная функция возвращает true.
          Оптимизированная функция тоже возвращает true.
          И что не так?
        • +1
          Если не дойдет — то все равно вернет true. Значит, если неопределенное поведение трактовать как возврат true, то цикл можно не выполнять.
        • +1
          Логика очень простая: во всех четырёх случаях функция возвращает true, а в пятом стреляет в ногу.
          Для длинного массива, может быть, компилятор и запарился бы доказывать теорему, а короткий — элементарно может развернуть и наступить.
  • +1
    В NaCl баге был просто неправильный код. Даже если бы переполнение было определено, баг бы остался.
  • +2
    Сам себе злобный буратина этот Регер
     while (a!= MAX && b!=MAX && с != MAX) {
    

    и всё будет нормально. Они написал невычислимую фукцию.
    Его цикл никогда не придёт к отрицательному выводу и будет крутить по кругу один и тот же миллиард операций. Компилятор поняв что тут никогда не может быть другого результата сделал так. Стрёмно конечно, но код кривой и за уши притянут.
    • +3
      А он и не хотел, чтобы цикл пришёл к отрицательному выводу (как и к положительному).
      Он объясняет, что для какого-то встроенного приложения ему нужно было написать на C++ бесконечный цикл так, чтобы оптимизирующий компилятор не смог удалить из программы весь код, следующий за циклом.
  • –24
    Столько холивара из-за
    int table[4];

    for (int i = 0; i <= 4; i++).

    В общем, написать i < 4 и дело с концом. Обычная реальная человеческая ошибка. Точка.
  • –9
    Минусующие, умоляю, напишите пожалуйста ниже, в чем мое заблуждение? Я заинтригован.
    • +22
      Вероятно, минусы ставят потому, что темой дискуссии является не ошибка как таковая, а философия компиляции, скрывающаяся за ней.

      В двух словах: компилятор может сгенерировать код СОВЕРШЕННО не такой, которого ждет/подразумевает программист.
  • +1
    По-моему уже давно назрело создание виртуальной машины C/C++, в которой исполнялся бы не машинный код, а некий псевдокод, прямо отображающий конструкции языка. И каждый раз, сталкиваясь с неопределенным поведением, такая виртуальная машина должна печатать предупреждения. Иначе отладка таких случаев превращается в сущий кошмар.

    Например, многие игнорируют то, что в C и C++ вычитание или сравнение указателей, указывающих на элементы разных массивов, приводит к UB. Они справедливо полагают, что поскольку адресное пространство процесса в современных ОС является линейным, и поскольку указатели являются 32- или 64-битными числами — то их можно сравнивать с корректным результатом в любом случае. И это действительно корректно работает с современными компиляторами. Но стоит появиться такому компилятору, который увидит в таком сравнении UB и воспользуется этим для «оптимизации» — то полетят такие программы, как спелые груши. А как обнаружить все такие случаи в программе?
    • НЛО прилетело и опубликовало эту надпись здесь
      • 0
        есть реальность (организация адресного пространства и прочее), которая никуда не собирается исчезать

        NUMA нереальна?
        И сегментированная адресация в x86 тоже нереальна?
        • НЛО прилетело и опубликовало эту надпись здесь
          • +2
            Когда-то программисты рассчитывали на то, что адресное пространство заворачивается по границе сегмента. А потом в операционных системах появлялись костыли для прикладного софта, чтобы последний работал.
            • НЛО прилетело и опубликовало эту надпись здесь
              • +3
                На то стандарт и есть, чтобы быть независимым от реализации.

                А если по-вашему, то не будет никакого C++. Будет миллион совершенно разных языков: С++Microsoft, С++ARM, С++DOS и т.д. И когда через 5 лет все перейдут с x86 на какую-то новую мега-архитектуру, просто перекомпилировав свой код, вы так и останетесь с вашим кодом, полагающимся накие-то особенности конкретной реализации.
                • НЛО прилетело и опубликовало эту надпись здесь
                  • +2
                    Мысль проста: но особенности конкретной реализации UB, какими бы логичными и здравомыслимыми они бы не казались, полагаться нельзя. А раз полагаться нельзя (и ни один добросовестный программист этого делать не должен), то почему бы не использовать эту свободу для оптимизации?
                    • 0
                      Потому что такая «оптимизация» в подавляющем большинстве случаев приводит к поведению, которого программист ЗАВЕДОМО не желал. Зачем в таком случае продолжать компиляцию? Лучше было бы вывести сообщение об ошибке.
              • +4
                Кстати, хочу напомнить знаменитую историю с memcpy/memmove в glibc (это GNU реализация стандартной библиоеки языка С).

                Cогласно стандарту копирумые регионы памяти в memcpy не должны пересекаться, иначе это UB. В memmove же регионы могут пересекаться.

                Когда-то много-много лет назад в glibc реализовали memcpy как алиас на memmove. Это не было никаким нарушением. И вот несколько лет назад авторы решили зделать новую, оптимизированную реализацию memcpy. Эта новая реализация тоже соответствовала стандартам.

                Что же получилось? Оказалось, что очень много кода взяло и сломалось. Куча программистов в течение многих лет, исповедуя такие взгляды как вы, полагались на конкретную реализацию UB и передавали в memcpy пересекающиеся регионы. А что такого, работало же!

                Был большой скандал. В результате из-та таких програмистов пришлось откатить оптимизираванную версию memcpy.
                • НЛО прилетело и опубликовало эту надпись здесь
                • +2
                  В результате из-та таких програмистов пришлось откатить оптимизираванную версию memcpy.

                  Если бы! Потом вернули её обратно, признав главенство стандарта над совместимостью с криворукими программистами.
                  Полная история: avva.livejournal.com/2323823.html
                  • +1
                    Верно, подзабыл некоторые детали.
                  • –1
                    В данном конкретном случае кривую программу можно исправить с помощью #define memcpy memmove.
                    • +1
                      Это если у вас есть исходники.
                      • +1
                        Для программ без исходников всё ещё проще: они по прежнему будут вызывать memmove независимо от их желания.
                        • +1
                          Это только если libc статически вкомпилирован в софт, что в большинстве случаев естественно не так.
                          • +2
                            Это только если компилятор не инлайнит всякую мелочь из libc прямо в код.
                            • 0
                              Не думаю, что компилятор/линкер не имеет право инлайнить код из динамической библиотеке так как это сломает ABI и не позволит, например, предсказуемо заменить код для всех пользователей этой библиотеки.
                              • +3
                                Максимум моральное право. Стандарт не требует, чтобы рантайм находился именно в динамической библиотеке и вообще каких-то там либц. У того же gcc есть внушительный список функций из стандартной библиотеки, которые по умолчанию реализованы как встроенные и раскрываются в кусочки ассемблера прямо в коде. Конечно, это можно отключить с помощью -fno-builtins, но ведь об этом тоже надо подумать.
                      • +1
                        Если у вас нет исходников, то что же вы собрались компилировать с glibc?
                        • +3
                          Судя по описанию по ссылке, проблемы были с libc.so (динамической библиотекой) и с адобовским флеш-плеером, поставляемым без исходников.
                  • НЛО прилетело и опубликовало эту надпись здесь
                    • +4
                      А я вполне понимаю.

                      Microsoft из кожи вон лезла, чтобы существующие приложения для её предыдущих ОС продолжали работать и на новых — невзирая на все ошибки, допущенные программистами старых приложений.
                      Они понимали, что пользователи выбирают ОС для того, чтобы работали их приложения — а не наоборот.
                      Результат — Microsoft давно и прочно лидирует на этом рынке.

                      Подробнее: habrahabr.ru/post/103598/
                      • НЛО прилетело и опубликовало эту надпись здесь
                    • +1
                      Я как раз очень уважаю Линуса за прагматический, а не религиозный подход к разработке ПО. По-моему он и люди придерживающиеся его подхода сделали для области в целом, и для свободного ПО в частности, больше чем Святой Столлман и его апостолы, которые готовы пожертвовать всем, ради идеологической чистоты и религиозных догм :)
                      • НЛО прилетело и опубликовало эту надпись здесь
                        • +1
                          Понятно, что все зависит от ситуации. Но ломать совместимость на уровне ОС, обрушивая кучу работающего софта, и все ради маленькой оптимизации — это по-моему уже религиозный фанатизм. Да, это ошибка криворукий програмистов, которые писали плохой софт несоотвествующий стандарту, но при этом Линус прав, что не надо за это наказывать конечных пользователей.
                          • НЛО прилетело и опубликовало эту надпись здесь
                            • +2
                              Для конечного пользователя, конечно важнее, что бы работал флэш, а не дополнительные 0.7% прироста производительности системы. Линус, кстати, вовсе не защищал криворуких разработчиков (которых сам материл), а конечных пользователей, и в этом он совершенно прав, ИМХО.
                              • НЛО прилетело и опубликовало эту надпись здесь
                                • +3
                                  Так оптимизация glibc выполняется ради спортивного интереса, или ради удобства конечных пользователей?

                                  Если glibc стала «работать быстрее», но при этом программы пользователей работать перестали — в чём цель такой оптимизации? Поиграть мускулами «смотрите какие мы крутые программисты»?
                                  • НЛО прилетело и опубликовало эту надпись здесь
    • +5
      Примерно такой инструмент уже есть. Называется UB Sanitizer, включён в GCС >=4.9 и Clang >=3.3. Идея состоит в том, что компилятор в местах, которые могут привести к UB, вставляет в код проверки, и если во время выполнения эти проверки срабатывают, программа падает с сообщением об ошибке.
      (Этот ответ также для mifki и zuborg и другим)
    • +1
      Он ничего не даст, потому что во-первых: UB может появиться в середине процесса оптимизации, а во-вторых: такой инструмент вселит ложную уверенность в качестве кода. Он может показывать, что все хорошо, тогда как реальная программа окажется уязвимой.

      Такие инструменты есть, но нельзя рассчитывать на то, что они выявят все проблемы и избавят программиста от необходимости думать.
  • +1
    Я считаю, что компиляторы, которые так агрессивно «оптимизируют» код с неопределенным по стандарту поведением, хоть формально и действуют в рамках стандарта, но практически действуют злонамеренно, приводя к труднообнаружимым сбоям программ.

    Если компилятор видит, что в каком-то месте программы возникает неопределенное стандартом поведение — то это может означать одно из трех:
    1) программист допустил ошибку;
    2) программист считает, что неопределенное поведение все же определено в каких-то рамках на данной платформе;
    3) программист полностью осознает, что поведение никак не определено и готов к абсолютно любому результату;

    Так вот, на практике обычно имеют место случаи 1) и 2). В случае 1) компилятору следует, вместо «оптимизации», прервать компиляцию с сообщением об ошибке. В случае 2) компилятору следует честно скомпилировать программу исходя из того поведения, которое было доопределено для данной конструкции сверх стандарта, исходя из здравого смысла. Случай 3) в моей практике не возникал никогда, и я думаю, что он крайне маловероятен. И действия компиляторов, которые исходят из этого маловероятного случая, следует считать как минимум неадекватными, а как максимум — злонамеренными.
    • +10
      Опять двадцать пять. В том то и проблема, что компилятор НЕ ВИДИТ и не отличает одну ситуацию от другой.

      Пользователь написал функцию. Компилятор прогнал DCE, часть кода ушла. В теле функции обнаружился цикл — прогнали LICM. Потом все это было заинлайнено в цикл другой фукнции, которая была заинлайнена в цикл третьей фукнции где была сделана векторизация/разворачивание/пилинг цикла, а данные четырежды подвергались alias анализу.

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

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

      Еще раз — отличить UB от не UB в реальном коде практически невозможно, потому что реальный код это не «hello world» на 5 строк.

      P.S.: В следующей статье по llst видимо придется затронуть этот момент, иначе холивор так и будет продолжаться.
      • +4
        В том то и проблема, что компилятор НЕ ВИДИТ и не отличает одну ситуацию от другой.

        В примере с i <= 4 компилятор ВИДИТ что имеет место выход за пределы массива. В примере с reallock() компилятор ВИДИТ что имеет место использования ptr после reallock(). В обоих случаях без «ВИДИТ» компилятор не имеет права модифицировать поведение кода.
        • +2
          В данном конкретном случае — может быть. Компилятор много чего может видеть, но обязан он делать только то, что предписано стандартом. А в стандарте сказано, что обращение к памяти за пределами объекта не определено.

          В частности, §5.7 стандарта C++11:
          …moreover, if the expression P points to the last element of an array object, the expression (P)+1 points one past the last element of the array object, and if the expression Q points one past the last element of an array object, the expression (Q)-1 points to the last element of the array object. If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined.
          • +1
            В данном конкретном случае — может быть.
            Повторюсь — если бы не видел, то он не имел бы права сделать свою агресивную оптимизацию.

            Никто ж не спорит, что компилятор никому ничего не обязан, если он строго соотвутствует стандарту. Вопрос в том, как ему следует поступать в случаях, когда возможны варианты. Текущее поведение неудовлетворительное и приводит к дополнительным багам в программах.
          • +1
            Речь идёт как-раз о том, что надо поменять стандарт реагирования на обнаруженные случаи неопределённого поведения программы. Вместо оптимизаций, представленных в статье, нужно генерить ругань и уведомления. С трудом представляю себе, где такие оптимизации вообще могут быть полезны.

            Упомянутый тут UB Sanitizer вот — это хорошо.
      • 0
        Тут же речь идет о том, что компилятор УЖЕ опознал один из этих трех случаев, но какой конкретно, он сказать не в состоянии. При этом два из них или ошибочные или подозрительные, да и встречаются чаще. Почему же при этом компилятор выбирает самую худшую стратегию поведения — молча сделать что-то, а не выдать хотя бы информационное сообщения: я делаю то-то, потому что?
    • +5
      (В дополнение в комментарию Halt )

      На самом деле есть четвертый вариант:
      4) Формально UB в некоторой конструкции возможно, но логика программы такова, что оно никогда не реализуется.
      И этот случай на самом деле самый частый. Встречается чуть ли не важдой второй строчке.
      Пример:

      for (int i = 0; i < 42; ++i)
      {
          do_something(i);
      }
      

      В выражении ++i возможно UB. Хотим ли мы, чтобы компилятор вставил проверку или выдавал предупреждение? Ведь тут нет никакого UB, т. к. i никогда не достигнет INT_MAX.

      Или всё-таки достигнет? Что, если do_something получает i по ссылке и изменяет его значение? Как компилятор может это понять? Только в процессе преобразований кода, то есть в процессе оптимизации.
      • –3
        На самом деле есть четвертый вариант:
        4) Формально UB в некоторой конструкции возможно, но логика программы такова, что оно никогда не реализуется.

        Нет, этого варианта нет, поскольку в вашей статье (и моих ответах) рассматриваются случаи, когда UB обязательно возникает, и компилятору это известно на этапе компиляции.
        • +2
          Последний разобранный пример — именно об этом: чтобы узнать, есть UB или нет, компилятору пришлось бы доказывать теорему Ферма.

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

            Либо компилятор реализует определенное в стандарте поведение, либо он реализует каким-то выгодным для себя образом неопределенное поведение. Но для того, чтобы получить свободу в реализации неопределенного поведения, компилятор сначала должен доказать, что неопределенное поведение имеет место. В противном случае это будет неверный результат компиляции.
            • +1
              Смотрите ещё раз: вот у нас есть код
              for (int i = 0; i < 42; ++i)
              {
                  do_something(i);
                  if(i==INT_MAX) can_eliminate_this_call();
              }
              


              Стандарт говорит: «если i в операторе ++i принимает значение INT_MAX, то результат операции допускается любой».

              Вы говорите: «компилятор обязан проверить, не собирается ли do_something() присвоить i значение INT_MAX, прежде чем удалить вызов can_eliminate_this_call()».

              Компилятор говорит: «да чего мне париться? если i принимает значение INT_MAX, то я могу удалить вызов can_eliminate_this_call(), потому что UB, а если не принимает, то я могу удалить этот вызов, потому что условие ложно. Даже заглядывать внутрь do_something() незачем!»
              • –2
                да чего мне париться? если i принимает значение INT_MAX, то я могу удалить вызов can_eliminate_this_call(), потому что UB, а если не принимает, то я могу удалить этот вызов, потому что условие ложно

                Формально да, компилятор может это сделать. Но фактически очень сомнительно, что именно этого хотел программист. Зачем тогда компилятор должен делать так, как вы описали? Чтобы наказать программиста в изощренной форме труднообъяснимыми ошибками за UB?
                • 0
                  Но фактически очень сомнительно, что именно этого хотел программист

                  Вы опять возвращаетесь к предположению, что компилятор должен играть в телепата
                  • –1
                    В вашем случае компилятор как раз и играет в телепата. Любое «неопределенное поведение», описанное в стандарте, тем или иным образом определяется во время компиляции программы.
            • +3
              Но для того, чтобы получить свободу в реализации неопределенного поведения, компилятор сначала должен доказать, что неопределенное поведение имеет место.
              Никому он ничего не должен. Простейший пример: «1 << 129». В «железе» это будет 2 на x86 и 0 на ARM'е. И что с этим значением должен делать компилятор? А кросс-компилятор? А если это явным образом costexpr функция? Да, можно доопределить всё тем или иным способом. Можно вввести 100500 «правил игры». А можно просто сказать: это не задача компилятора и программист должен как-то сделать так, что никому не будет интересно чему равно «1 << 129». Просто сделать так, чтобы этого не случалось.

              В конце-концов никому не приходит в голову обвинять компилятор в том, что следующий код имеет привычку падать:
              char *a=0x1234;
              free(a);

              Почему в других подобных случаях (с теми же сдвигами) это приводит к истерике и каким-то странным воплям?

              P.S. Заметьте, что в случае с free вы вполне можете передать в функцию NULL и всё будет законно. Потому что так говорит стандарт C++. А в программах для POSIX и/или Win32-систем вы можете-таки сравнивать указатели на разные массивы (так как этими стандартами гарантируются плоское адресное пространство), а вот как раз в DOS (скажем в Large модели) у вас вполне могут быть разные указатели, которые физически указывают на один участок памяти. И там разного рода трюки реально могут приводить к известного рода проблемам.

              Никто не говорит, что «доопределение» стандарта — это плохо. Нет, это вполне нормально. Что плохо — так это требовать от компилятора чтобы он «угадывал» что вы там имеете в виду. Вот это да — это качмар. Это — дорога в ад. Все «доопределения» должны быть явно описаны в документации на компилятор, вот и всё.
              • 0
                это не задача компилятора и программист должен как-то сделать так, что никому не будет интересно чему равно «1 << 129». Просто сделать так, чтобы этого не случалось

                Такой вариант я могу принять. Но в этом случае компилятор должен печатать сообщение об ошибке и уж по крайней мере не пытаться что-то додумать за программиста, будто ему совершенно все равно, что произойдет с программой в случае UB. Зачем нужны такие оптимизации, которые приводят только к ошибкам, а не к ускорению программы?
              • –1
                а вот как раз в DOS (скажем в Large модели) у вас вполне могут быть разные указатели, которые физически указывают на один участок памяти. И там разного рода трюки реально могут приводить к известного рода проблемам.

                Что если я пишу код с использованием Win32 API, который заведомо никогда нельзя будет не то что запустить под DOS, а даже скомпилировать под нее? Я должен отказаться от тех преимуществ, которые дает сравнение линейных указателей? Но это приведет к менее эффективному коду. Создание портируемых программ — сложная задача, и ее решение требует жертв в эффективности. Этим можно заниматься ради спортивного интереса, но когда конкурент предлагает непортируемую, зато более быструю программу — клиентов сложно привлечь тем, что «но зато моя программа не использует UB!». Какая клиентам разница, если обе программы работают стабильно, а исходный код ни той, ни другой недоступен?
                Никто не говорит, что «доопределение» стандарта — это плохо. Нет, это вполне нормально. Что плохо — так это требовать от компилятора чтобы он «угадывал» что вы там имеете в виду. Вот это да — это качмар. Это — дорога в ад

                Угадывание компилятора в любом случае имеют место. В вашем варианте компилятор предполагает, что результат выполнения операции с UB (да и поведения всей программы после единичного случая UB) может быть совершенно произвольным, и программисту до него нет дела. Компилятор считает, что вы имеете в виду именно это.
                • 0
                  Не могу себе представить программы, в которой программисту нужен был бы UB
                  • 0
                    Нужно не «неопределенное поведение», а платформо-специфичное поведение, которое в стандарте не определено, а реализуется компилятором по соглашению или де-факто.
        • +3
          Хотел бы заметить, что вы упорно путаете меня с автором статьи.
    • +3
      И ещё:

      В случае 2) компилятору следует честно скомпилировать программу исходя из того поведения, которое было доопределено для данной конструкции сверх стандарта, исходя из здравого смысла.

      Означает ли это, что вы предлагаете, что компилятор должен пытаться угадать, что хотел получить программист, и пытаться сгенерировать соответствующий код?

      Я считаю, что такое поведение будет как минимум не менее опасным.
      • –3
        Компилятор просто не должен пользоваться UB для модификации поведения программы (если не попросят). Не модифицировать код из-за выхода за пределы массива. Не игнорировать значение ptr, даже если оно продолжает использоваться после realloc(). Именно этого ожидает программист, когда внутри своего мозга моделирует поведение программы.
        • +1
          А как только выйдет какой-нибудь стандарт C++100500, который именно так изменит язык, так и компиляторы начнут ему соответствовать.

          Или разработчики компилятора могут добавить сами в описание стандарты что-нибудь (например GCC позволяет делать type puning через union).

          Но говорить, что «раз программист, скорее всего, имел в виду не A, а B» нельзя: в мозги к программисту не залезешь и у всех могут быть разные идеи о том «как устроен мир».
      • 0
        Означает ли это, что вы предлагаете, что компилятор должен пытаться угадать, что хотел получить программист

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

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

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

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

        Если компилятор установит, что переполнение обязательно произойдет (оба аргумента известны на этапе компиляции) — что он должен делать? Есть вариант — сгенерировать ту же ассемблерную команду сложения, которая исполнится и вернет какой-то результат. Другой вариант — «оптимизировать», удалить команду сложения вообще. Я считаю, что здравому смыслу соответстувует первый вариант.

        Если выполняется сдвиг влево 32-битного числа на 32 и более бит — следует вернуть нуль.

        И т.д.

        Во всех случаях «оптимизации», описанных вами, компилятор знал, что поведение каких-то конструкций заведомо не определено, и пользовался этим знанием для удаления кода, реализующего такие конструкции, и замены их результата константами. Столкнувшись с неопределенным поведением, компилятор в любом случае делает какие-то предположения о том, что же на самом деле хотел получить программист. Любое неопределенное поведение получает какое-то определение на этапе компиляции. Но вот этот подход «оптимизации», применение которого вы описали — он контрпродуктивен. Потому что результат, «угаданный» компилятором, почти наверняка окажется не тем, что хотел получить программист. А раз есть такая уверенность — то следует печатать сообщение об ошибке, а не продолжать работу.
        • +1
          Можно в этом случае сравнить числа-указатели и вернуть результат сравнения, каким бы он ни был; а можно вернуть always-true и за счет этого сократить в программе ряд ветвлений и циклов. Как следует поступить?


          Ни так и ни так. Следует написать что-то типа:

          Warning: unrelated pointers comparision at line 156: a pointer based upon to a[] is compared to a pointer based upon b[]


          Или да, даже ошибка…
        • 0
          Если компилятор установит, что переполнение обязательно произойдет (оба аргумента известны на этапе компиляции) — что он должен делать? Есть вариант — сгенерировать ту же ассемблерную команду сложения, которая исполнится и вернет какой-то результат. Другой вариант — «оптимизировать», удалить команду сложения вообще. Я считаю, что здравому смыслу соответстувует первый вариант.
          Ok. Принято.

          Если выполняется сдвиг влево 32-битного числа на 32 и более бит — следует вернуть нуль.
          Хотя процессор-то сделает совсем не так. «В железе» «1 << 129» будет равна 2 на x86 и 0 на ARM'е, а вот «1 << 257» будет уже равна 2 и там и там. И как должен себя вести виндовый кросс-компилятор для платформы Android? Чему в нём должен быть результат работы выражения «1 << 129»?

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

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

            Если бы «никогда и ни при каких условиях» — то от таких оптимизаций не было бы никакого вреда. А он есть, вся статья о нем, с примерами.
            Хотя процессор-то сделает совсем не так. «В железе» «1 << 129» будет равна 2 на x86

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

            Тут следует задуматься, почему и с какой целью в стандарте было введено UB для конкретных случаев. Скажем, целочисленное переполнение. Поведение не было определено для того, чтобы программу можно было максимально эффективно перевести в целевой ассемблер. Большинство процессоров при переполнении производит «Wrap around», но некоторые делают «Saturation», еще какие-то бросают исключения. Если бы в стандарте было жестко задано только «Wrap Around» — то на тех процессорах, где используется «Saturation», каждая операция сложения должна была бы снабжаться кодом эмуляции неестественного для этих процессоров поведения Wrap Around. Также было бы невозможно воспользоваться желаемым (в некоторых случаях) поведением типа «Saturation».

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

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

            Допустим, ваш компилятор жестоко наказывает вас за каждое целочисленное переполнение. Результат работы вашей программы при переполнениях становится совершенно непредсказуемым, что вынуждает вас переделать ее таким образом, чтобы избежать переполнений любой ценой. И что получится в результате? Ваша программа обрастет огромным количеством проверок. И вы не сможете воспользоваться поведением процессора при переполнениях. Вам придется эмулировать это поведение сложными конструкциями, которые будут медленно работать. Скорее всего придется повысить разрядность используемых чисел сверх необходимого. А это — расход ценных регистров процессора.
            И как должен себя вести виндовый кросс-компилятор для платформы Android? Чему в нём должен быть результат работы выражения «1 << 129»?

            Я бы сделал эмуляцию того «железного» результата, который получается на целевой платформе. Если это невозможно — то можно попытаться как-то доопределить поведение компилятора. Но таким образом, чтобы полученный результат был максимально осмысленным.
            • +3
              Вы знаете, по-моему программистам, которые закладываются некие особенности платформы (endianess, поведение при переполнении, и т.д.), делают необоснованные предположения о размере базовых типов (например в хардкоде предполагают, что int — это 4 байта, а long — 8), используют без надобности платфро-зависимыми конструкциями и типами данных (не раз встречал програмистов-виндузятников, считающих BYTE и WORD — родными С-шными типами данных) — всем им надо отрывать руки по плечи в выгонять вон из профессии. И говорю я это не потому что я поборник религиозной чистоты (вовсе нет), а потому что я писал на практически всех возможных платформах, включая десктопы и embedded на кучу операционных систем, и делал сам и консультировал кучу проектов портирования платформы и/или ОС и/или компилятора. И потому что 80% процентов всех проблем в таких проектах — это вовсе не серьёзные различия в API и работе железа, а банальная нечистоплотнрсть и халтура программистов, любимая отмазка которых была «этот код никогда не надо будет портировать» и «вроде работает более или менее, и ладно, ХЗ почему».
              По-моему это базовая часть профессионализма и аккуратности — писать платформо-независимый код везде где можно, даже если в данную минуту это не обязательно (но почти всегда окупается в дологосрочной перспективе), а платформо-зависимые куски заворачивать в ifdef-ы, выделять в отдельные модули, и вообще хорошо понимать, что в них происходит. Опять таки по-своему опыту могу сказать, что это никак не влияет на скорость выполнения, и почти не замедляет написание кода, не больше чем остальные принятые практики написания качественного и аккуратного (не write-only) кода.

              ЗЫ: насчет «низкоуровнего» и «не портируемого в принципе» кода, в котором вы считаете, что допустимо можно закладываться на особенности железа — у меня один из больших последних проектов, это было портирование сложного embedded софта c эзотерической железки, в которой даже ОС не толком не было, на линукс/gcc. Там, правда, к счастью программисты были толковые и хотя лет 15 назад, когда это начинали писать, никто не думал про портирование — тем не менее код был по большей частью был написан качественно и портировался без проблем. Но если бы они забили и решили, что писать аккуратно/портируемо (а это таки почти синонимы, по-моему) не надо — то проект из сложного, превратился бы в кошмарный.
              • 0
                Чисто ради любопытства хочу поинтересоваться. Когда вам нужны целые длиной не менее 32 бит — вы каким типом данных пользуетесь, int или long? Пожалуйста ответьте честно, не на основе того, «как должно быть», а на основе того, как есть на самом деле.
                • +3
                  Не то ни другое естественно. Если мне важен физический размер переменной, то я воспользуюсь чем нибудь вроде uint32_t а не буду уповать на звезды и настройки компилятора, что int выйдет длиной именно в 32 бита.
              • –1
                всем им надо отрывать руки по плечи в выгонять вон из профессии

                Пожалуйста не приписывайте мне чужих недостатков. Ваше негодование по поводу кода некоторых программистов не имеет никакого отношения к описанной в моем посте ситуации.

                Вы так и не ответили по существу насчет целочисленного переполнения. Допустим, что платформо-зависимый код, использующий эффекты переполнения на конкретной платформе, изолирован в виде отдельных файлов и снабжен соответствующими комментариями. Вы по-прежнему считаете, что такой код совершенно недопустим в C-программах? И надо использовать исключительно определенные стандартом конструкции, исключающие переполнение? Как быть в таком случае с эффективностью программы? Писать код, использующий платформо-зависимые эффекты переполнения, на ассемблере?
                • +1
                  Пожалуйста не воспринимайте моё негодование не свой счет, я понятное дело, понятия имею что и как вы пишите, а критика относилось не к вам, а к позиции, которую вы защитите, и применение которой, я считаю признаком плохого кода.

                  Насчет платформо-зависимого кода, естественно он всегда будет. Но стоит различать платформо-зависимый код в рамках языка, который достаточно обернуть в ifdef и/или выкинуть в отдельный модуль, и целенаправленная игра с огнём, в виде использования UB в своих целях. Можно конечно поместить такой код в отдельный файл, вырубить для него все оптимизации, написать крупным буквами «ОСТОРОЖНО, СТРЕЛЬБА В НОГУ!!! 11АДЫНАДЫН», проверять вручную сгенерированный компилятором код, но при этом надо помнить, что от любого чиха, апдейта компилятора/библиотек или настроек системы все может сломаться и вам нужно постоянно гонять regression tests на весь этот код. Если эти оптимизации того стоят, то флаг вам в руки, но что то мне подсказывает, что в большинстве случаев накладные расходы на поддержание такого кода перевесят все выгоды от него.
                  • –1
                    Если вы разрабатываете встроенный софт для устройства, которое имеет четко определенную платформу и компилятор, и время жизни на рынке такого устройства составит всего несколько лет, и если после выпуска нет необходимости обновлять ПО — то все упомянутое вами вполне допустимо, не приводит к затратам на поддержание, и позволяет решить задачу быстро.

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

                    А на рынке — как на войне. Неважно, что вы победили за счет «нечестного трюка» и перерезали врагов ночью во сне, а не перебили в честном бою. Главное — кто победил, а не каким образом.
                • +2
                  Вы немного недооцениваете компиляторы.

                  Например, во сколько инструкций будет скомпилировано вот это на x86:
                  y = (x << 5) | (x >> 27);
                  


                  Ответ
                  Правильный ответ - в одну. Циклический сдвиг. (Верно для GCC, Clang, ICC)
                  • 0
                    Это, конечно, очень круто. Но такие вещи характерны для современных крупных компиляторов. Их количество по пальцам можно пересчитать. Такие компиляторы появились недавно, а на многие встраиваемые платформы, микроконтроллеры и т.д., их до сих пор не существует. Там гораздо более слабые компиляторы, поэтому никаких других средств эффективно реализовать платформо-зависимые вещи, типа поведения при переполнении и т.д., там не существует, кроме использования поведения языка, не определенного в стандарте. И для ПК таких средств тоже не существовало до нескольких последних лет.
                    • 0
                      Насколько я знаю, большинство кода для встраиваемых систем сегодня компилируется на gcc, который сам не вчера появился, хотя действительно не факт, что для всех архитектур поддерживаются все хитрые оптимизации с популярных платформ. А на ПК, оптимизирующие компиляторы существуют уже полтора десятка лет, если не больше, и уж никак не «последние пару лет».
                      • 0
                        Насколько я знаю, большинство кода для встраиваемых систем сегодня компилируется на gcc

                        Можете ли привести источники этой информации? Я вот разрабатываю код для встраиваемых систем, и gcc применяется только на одной из них.
                        А на ПК, оптимизирующие компиляторы существуют уже полтора десятка лет

                        Я видел оптимизирующие компиляторы, созданные еще в 70х гг 20 века. Все дело только в степени оптимизации, которую они осуществляют. Достаточна ли она для того, чтобы можно было без ущерба для производительности программы избавиться, например, от целочисленных переполнений? Вы будете утверждать, что компиляторы конца 90х были на это способны?
                  • +1
                    Только что проверил на Visual Studio 2013
                    Функция
                    uint8_t add_oflow(uint8_t a, uint8_t b)
                    {
                        uint16_t c = (uint16_t) a + (uint16_t)b;
                        return (uint8_t)(c & 0xFF);
                    }
                    
                    
                    избегает неопределенного поведения, при этом эмулируя переполнение типа Wrap-around

                    Она действительно компилируется в однобайтовые инструкции сложения на процессоре.

                    Это прекрасно. Действительно, на современных крупных компиляторах предлагаемый вами подход (всегда избегать неопределенного стандартом поведения) применим.
            • +4
              Но в любом случае надеюсь, что вы меня поняли, что я поддерживаю те варианты доопределения случаев UB, которые максимально соответствуют результатам выполнения этих запрещенных операций в железе.
              Я очень рад за вас и надеюсь посмотреть на ваш новый язык когда вы его сотворите. Но это не будет языком C и/или C++!

              Вариант, о котором вы говорите в стандарте C тоже есть — это «implementation-defined bahavior». Он, например, случается тогда, когда вы преобразуете int в uint. Стандарт гарантирует, что ничего не взорвётся и что программа не отформатирует винчестер. Она что-то сделает, причём, что важно, для одного и того же компилятора это будет всегда один и тот же результат, но какой именно — зависит от конкретной реализации.

              В этой ситуации программист, пишущий код, который заведомо приводит к целочисленным переполнениям, сознательно ограничивает работоспособность своего кода только целевой платформой.
              Нет, в этом случае создаёт себе проблему в виде программы, которая не является программой на языках C и/или C++. Что и как это чудо будет делать разработчиков компилятора не интересует. Причём не интересует от слова «совсем».

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

              Допустим, ваш компилятор жестоко наказывает вас за каждое целочисленное переполнение.
              Ну зачем же за каждое то наказывать? Это случается только тогда, когда это может сделать вашу программу быстрее. Ну там извести проверку на окончание цикла можно или ещё чего.

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

              И что получится в результате? Ваша программа обрастет огромным количеством проверок. И вы не сможете воспользоваться поведением процессора при переполнениях. Вам придется эмулировать это поведение сложными конструкциями, которые будут медленно работать. Скорее всего придется повысить разрядность используемых чисел сверх необходимого. А это — расход ценных регистров процессора.
              Ну что ж поделать. Но если у вашего компилятора нет флага, аналогичного, GGC'шному -fwrapv, то у вас нет выбора. Если у вас есть код, который вызывает в некоторых условиях UB, то у вас нет программы на языке C или C++. То, что у вас есть — это бомба замедленного действия, которая грозит взорваться при выходе новой версии компилятора. Потому что компиляторы научаются пользоваться тем фактом, что программа на языке C или C++ никогда и ни при каких условиях не вызывает UB всё лучше и лучше.

              Ещё раз: вы путаете «undefined behavior» и «implementation-defined behavior». Последнее — это то, где компилятор может творить вольности, но такие места в программе допустимы. Первого не должно быть. Никогда и ни за что. Если вы начнёте жаловаться на то, что компилятор вашу программу с UB компилирует как-то не так, то разработчики в лучшем случае напишут в баге почему вы неправы и закроют с резолюцией RESOLVED INVALID.

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

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