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

Стандарт C явно определяет переполнение целого числа со знаком как наличие неопределенное поведение. Тем не менее, большинство ЦП реализуют арифметику со знаком с определенной семантикой для переполнения (за исключением, возможно, переполнения деления: x / 0 и INT_MIN / -1).

Разработчики компиляторов используют неопределенность таких переполнений для добавления более агрессивных оптимизаций, которые имеют тенденцию ломать устаревший код очень тонкими способами. Например, этот код мог работать на старых компиляторах, но больше не работает на текущих версиях gcc и clang:

/* Tncrement a by a value in 0..255, clamp a to positive integers.
   The code relies on 32-bit wrap-around, but the C Standard makes
   signed integer overflow undefined behavior, so sum_max can now 
   return values less than a. There are Standard compliant ways to
   implement this, but legacy code is what it is... */
int sum_max(int a, unsigned char b) {
    int res = a + b;
    return (res >= a) ? res : INT_MAX;
}

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

Я придумал этот вопрос, когда смотрел это: C++Now 2018: Джон Регер «Заключительный доклад: неопределенное поведение и оптимизация компилятора»

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

Комментарии не для расширенного обсуждения; этот разговор был перешел в чат.

Samuel Liew 09.05.2019 01:43

Причина, по которой C говорит, что переполнение целого числа со знаком не определено, заключается в том, что некоторые процессоры используют «дополнение до 2», некоторые используют «дополнение до 1», некоторые используют «знак и величину»; и во всех случаях переполнение может вызвать что угодно (например, процессоры, такие как MIPS, имеют «ловушку при переполнении»). Другими словами, речь идет о переносимости, а не об оптимизации.

Brendan 09.05.2019 01:53

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

user207421 09.05.2019 03:50

@ user207421: Да, это хороший вопрос, ответ на который, кажется, больше никогда. Следовательно, текущее предложение удалить поддержку недвух дополнительных представлений: open-std.org/jtc1/sc22/wg14/www/docs/n2330.pdf

chqrlie 09.05.2019 07:16

@Brendan: архитектура комплементов осталась в прошлом. Можно выбрать ловушку MIPS при переполнении.

chqrlie 09.05.2019 07:19

@chqrlie: C тоже ушел в прошлое (сейчас ему 47 лет). Есть много дизайнерских решений, которые имели смысл тогда, но больше не имеют смысла, но продолжают существовать, потому что изменение сломает слишком много существующего программного обеспечения.

Brendan 09.05.2019 22:41
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
37
6
903
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Не совсем пример оптимизации, но одним полезным последствием неопределенного поведения является -ftrapv переключение командной строки GCC/clang. Он вставляет код, который приводит к сбою вашей программы при целочисленном переполнении.

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

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

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

curiousguy 13.05.2019 09:30

Стандарт не зависит от того, следует ли считать конструкцию, вызывающую UB, ошибочной или непереносимой. Если программа никогда намеренно не обращается, например. a char[10][10] с внутренним индексом больше девяти, наличие ловушки реализации, если это так, может быть полезно. С другой стороны, некоторые задачи могут быть решены более эффективно в реализации, которая позволяет внутреннему индексу получать доступ к данным в нескольких строках, чем реализация, требующая от программиста замены цикла, перебирающего элементы в нескольких строках, двойным кодом. петля, что...

supercat 16.02.2022 22:05

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

supercat 16.02.2022 22:06

Ответ на самом деле в вашем вопросе:

Yet most CPUs implement signed arithmetics with defined semantics

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

Язык C был изобретен в 1972 году. Тогда еще существовали мейнфреймы IBM 7090. Не все компьютеры были двойками — комплимент.

Определить язык (и поведение переполнения) вокруг 2s-compliment было бы вредно для генерации кода на машинах, которых не было.

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

Если я правильно понимаю, что он предназначен для фиксации суммы a и b в 0....INT_MAX без зацикливания, я могу придумать два способа написать эту функцию совместимым образом.

Во-первых, неэффективный общий случай, который будет работать на всех процессорах:

int sum_max(int a, unsigned char b) {
    if (a > std::numeric_limits<int>::max() - b)
        return std::numeric_limits<int>::max();
    else
        return a + b;
}

Во-вторых, удивительно эффективный способ 2s-compliment:

int sum_max2(int a, unsigned char b) {
    unsigned int buffer;
    std::memcpy(&buffer, &a, sizeof(a));
    buffer += b;
    if (buffer > std::numeric_limits<int>::max())
        buffer = std::numeric_limits<int>::max();
    std::memcpy(&a, &buffer, sizeof(a));
    return a;
}

Получившийся ассемблер можно увидеть здесь: https://godbolt.org/z/F42IXV

Конкретный способ вашего дополнения 2 имеет другую семантику: он вернет INT_MAX для отрицательных значений a меньше -b.

chqrlie 09.05.2019 07:24

@chqrlie Должно быть, я неправильно понял требования функции. Я предположил, что, поскольку мы фиксировали положительные числа, входные данные гарантированно будут положительными.

Richard Hodges 09.05.2019 08:03

Глупая функция-пример проверяет только положительное переполнение, потому что b по своей сути является положительным, но a может иметь отрицательное значение. Это всего лишь пример, и ваша версия sum_max — это правильный способ достижения цели переносимым образом, но я видел, как устаревший код использует тест, который я опубликовал, который никогда не был правильным, но только что работал примерно 10 лет назад.

chqrlie 09.05.2019 08:08

Кстати, C++ совсем недавно удалил альтернативные представления целых чисел со знаком. дополнение two теперь является единственным поддерживаемым.

chqrlie 09.05.2019 08:10

@chqrlie Это верно для С++ 20, но я не упомянул об этом, потому что переполнение целых чисел со знаком остается UB по всем ранее упомянутым причинам, связанным с оптимизацией. Я не хотел создать неправильное впечатление, что устаревший код станет нормальным. Это не будет.

Richard Hodges 09.05.2019 08:20
Ответ принят как подходящий

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

Помимо оптимизации компилятора, необходимо учитывать еще один аспект. С UB вы получаете целые числа со знаком C/C++, которые ведут себя арифметически так, как вы ожидаете математически. Например, x + 10 > x остается верным сейчас (конечно, для действительного кода), но не при циклическом поведении.

Я нашел отличную статью Как неопределенное подписанное переполнение обеспечивает оптимизацию в GCC в блоге Кристера Вальфридссона, в которой перечислены некоторые оптимизации, учитывающие подписанное переполнение UB. Следующие примеры взяты оттуда. Я добавляю к ним примеры на С++ и ассемблере.

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

Если примеры выглядят бессмысленно (кто бы написал x * 10 > 0), имейте в виду, что вы можете очень легко добраться до такого рода примеров на C и C++ с помощью констант, макросов, шаблонов. Кроме того, к такого рода примерам может попасть компилятор при применении преобразований и оптимизаций в своей ИР.

Упрощение целочисленного выражения со знаком

  • Исключить умножение по сравнению с 0

    (x * c) cmp 0   ->   x cmp 0 
    
    bool foo(int x) { return x * 10 > 0 }
    
    foo(int):
            test    edi, edi
            setg    al
            ret
    
  • Убрать деление после умножения

    (x * c1) / c2 -> x * (c1 / c2) if c1 is divisible by c2

    int foo(int x) { return (x * 20) / 10; }
    
    foo(int):
            lea     eax, [rdi+rdi]
            ret
    
  • Устранить отрицание

    (-x) / (-y) -> x / y

    int foo(int x, int y) { return (-x) / (-y); }
    
    foo(int, int):
            mov     eax, edi
            cdq
            idiv    esi
            ret
    
  • Упрощение сравнений, которые всегда истинны или ложны

    x + c < x       ->   false
    x + c <= x      ->   false
    x + c > x       ->   true
    x + c >= x      ->   true
    
    bool foo(int x) { return x + 10 >= x; }
    
    foo(int):
            mov     eax, 1
            ret
    
  • Исключите отрицание в сравнениях

    (-x) cmp (-y)   ->   y cmp x
    
    bool foo(int x, int y) { return -x < -y; }
    
    foo(int, int):
            cmp     edi, esi
            setg    al
            ret
    
  • Уменьшить величину констант

    x + c > y       ->   x + (c - 1) >= y
    x + c <= y      ->   x + (c - 1) < y
    
    bool foo(int x, int y) { return x + 10 <= y; }
    
    foo(int, int):
            add     edi, 9
            cmp     edi, esi
            setl    al
            ret
    
  • Исключите константы в сравнениях

    (x + c1) cmp c2         ->   x cmp (c2 - c1)
    (x + c1) cmp (y + c2)   ->   x cmp (y + (c2 - c1)) if c1 <= c2
    

    The second transformation is only valid if c1 <= c2, as it would otherwise introduce an overflow when y has the value INT_MIN.

    bool foo(int x) { return x + 42 <= 11; }
    
    foo(int):
            cmp     edi, -30
            setl    al
            ret
    

Арифметика указателей и продвижение типов

If an operation does not overflow, then we will get the same result if we do the operation in a wider type. This is often useful when doing things like array indexing on 64-bit architectures — the index calculations are typically done using 32-bit int, but the pointers are 64-bit, and the compiler may generate more efficient code when signed overflow is undefined by promoting the 32-bit integers to 64-bit operations instead of generating type extensions.

One other aspect of this is that undefined overflow ensures that a[i] and a[i+1] are adjacent. This improves analysis of memory accesses for vectorization etc.

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

Это пример, когда изменение индекса с беззнакового на знаковый улучшает сгенерированную сборку:

Неподписанная версия

#include <cstddef>

auto foo(int* v, std::size_t start)
{
    int sum = 0;

    for (std::size_t i = start; i < start + 4; ++i)
        sum += v[i];

    return sum;
}

В случае unsigned необходимо учитывать случай, когда start + 4 переносится, и для этого случая создается ветка (ветки плохо влияют на производительность):

; gcc on x64 with -march=skylake

foo1(int*, unsigned long):
        cmp     rsi, -5
        ja      .L3
        vmovdqu xmm0, XMMWORD PTR [rdi+rsi*4]
        vpsrldq xmm1, xmm0, 8
        vpaddd  xmm0, xmm0, xmm1
        vpsrldq xmm1, xmm0, 4
        vpaddd  xmm0, xmm0, xmm1
        vmovd   eax, xmm0
        ret
.L3:
        xor     eax, eax
        ret
; clang on x64 with -march=skylake

foo1(int*, unsigned long):                             # @foo1(int*, unsigned long)
        xor     eax, eax
        cmp     rsi, -4
        jae     .LBB0_2
        vpbroadcastq    xmm0, qword ptr [rdi + 4*rsi + 8]
        vpaddd  xmm0, xmm0, xmmword ptr [rdi + 4*rsi]
        vpshufd xmm1, xmm0, 85                  # xmm1 = xmm0[1,1,1,1]
        vpaddd  xmm0, xmm0, xmm1
        vmovd   eax, xmm0
.LBB0_2:
        ret

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

#include <cstddef>

auto foo(int* v, unsigned start)
{
    int sum = 0;

    for (unsigned i = start; i < start + 4; ++i)
        sum += v[i];

    return sum;
}
; gcc on x64 with -march=skylake

foo(int*, unsigned int):
        cmp     esi, -5
        ja      .L3
        mov     eax, esi
        mov     eax, DWORD PTR [rdi+rax*4]
        lea     edx, [rsi+1]
        add     eax, DWORD PTR [rdi+rdx*4]
        lea     edx, [rsi+2]
        add     eax, DWORD PTR [rdi+rdx*4]
        lea     edx, [rsi+3]
        add     eax, DWORD PTR [rdi+rdx*4]
        ret
.L3:
        xor     eax, eax
        ret
; clang on x64 with -march=skylake

foo(int*, unsigned int):                              # @foo(int*, unsigned int)
        xor     eax, eax
        cmp     esi, -5
        ja      .LBB0_3
        mov     ecx, esi
        add     esi, 4
        mov     eax, dword ptr [rdi + 4*rcx]
        lea     rdx, [rcx + 1]
        cmp     rdx, rsi
        jae     .LBB0_3
        add     eax, dword ptr [rdi + 4*rcx + 4]
        add     eax, dword ptr [rdi + 4*rcx + 8]
        add     eax, dword ptr [rdi + 4*rcx + 12]
.LBB0_3:
        ret

Подписанная версия

Однако использование подписанного индекса приводит к красивому векторизованному коду без ветвей:

#include <cstddef>

auto foo(int* v, std::ptrdiff_t start)
{
    int sum = 0;

    for (std::ptrdiff_t i = start; i < start + 4; ++i)
        sum += v[i];

    return sum;
}
; gcc on x64 with -march=skylake

foo(int*, long):
        vmovdqu xmm0, XMMWORD PTR [rdi+rsi*4]
        vpsrldq xmm1, xmm0, 8
        vpaddd  xmm0, xmm0, xmm1
        vpsrldq xmm1, xmm0, 4
        vpaddd  xmm0, xmm0, xmm1
        vmovd   eax, xmm0
        ret
; clang on x64 with -march=skylake

foo(int*, long):                              # @foo(int*, long)
        vpbroadcastq    xmm0, qword ptr [rdi + 4*rsi + 8]
        vpaddd  xmm0, xmm0, xmmword ptr [rdi + 4*rsi]
        vpshufd xmm1, xmm0, 85                  # xmm1 = xmm0[1,1,1,1]
        vpaddd  xmm0, xmm0, xmm1
        vmovd   eax, xmm0
        ret

Векторизованные инструкции по-прежнему используются при использовании более узкого знакового типа:

#include <cstddef>

auto foo(int* v, int start)
{
    int sum = 0;

    for (int i = start; i < start + 4; ++i)
        sum += v[i];

    return sum;
}
; gcc on x64 with -march=skylake

foo(int*, int):
        movsx   rsi, esi
        vmovdqu xmm0, XMMWORD PTR [rdi+rsi*4]
        vpsrldq xmm1, xmm0, 8
        vpaddd  xmm0, xmm0, xmm1
        vpsrldq xmm1, xmm0, 4
        vpaddd  xmm0, xmm0, xmm1
        vmovd   eax, xmm0
        ret
; clang on x64 with -march=skylake

foo(int*, int):                              # @foo(int*, int)
        movsxd  rax, esi
        vpbroadcastq    xmm0, qword ptr [rdi + 4*rax + 8]
        vpaddd  xmm0, xmm0, xmmword ptr [rdi + 4*rax]
        vpshufd xmm1, xmm0, 85                  # xmm1 = xmm0[1,1,1,1]
        vpaddd  xmm0, xmm0, xmm1
        vmovd   eax, xmm0
        ret

Расчет диапазона значений

The compiler keeps track of the variables' range of possible values at each point in the program, i.e. for code such as

int x = foo();
if (x > 0) {
  int y = x + 5;
  int z = y / 4;

it determines that x has the range [1, INT_MAX] after the if-statement, and can thus determine that y has the range [6, INT_MAX] as overflow is not allowed. And the next line can be optimized to int z = y >> 2; as the compiler knows that y is non-negative.

auto foo(int x)
{
    if (x <= 0)
        __builtin_unreachable();
    
    return (x + 5) / 4;
}
foo(int):
        lea     eax, [rdi+5]
        sar     eax, 2
        ret

The undefined overflow helps optimizations that need to compare two values (as the wrapping case would give possible values of the form [INT_MIN, (INT_MIN+4)] or [6, INT_MAX] that prevents all useful comparisons with < or >), such as

  • Changing comparisons x<y to true or false if the ranges for x and y does not overlap
  • Changing min(x,y) or max(x,y) to x or y if the ranges do not overlap
  • Changing abs(x) to x or -x if the range does not cross 0
  • Changing x/c to x>>log2(c) if x>0 and the constant c is a power of 2
  • Changing x%c to x&(c-1) if x>0 and the constant c is a power of 2

Анализ цикла и оптимизация

The canonical example of why undefined signed overflow helps loop optimizations is that loops like

for (int i = 0; i <= m; i++)

are guaranteed to terminate for undefined overflow. This helps architectures that have specific loop instructions, as they do in general not handle infinite loops.

But undefined signed overflow helps many more loop optimizations. All analysis such as determining number of iteration, transforming induction variables, and keeping track of memory accesses are using everything in the previous sections in order to do its work. In particular, the set of loops that can be vectorized are severely reduced when signed overflow is allowed.

"х + 10 > х теперь верно" - нет, это не так. Он может отформатировать ваш жесткий диск, если x окажется равным INT_MAX-5.

user253751 09.05.2019 04:33

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

chqrlie 09.05.2019 07:50

очаровательный. хорошая находка.

Richard Hodges 09.05.2019 08:18

@immibis Я имел в виду, что это верно для всего действительного кода. UB недействительный код.

bolov 09.05.2019 11:41

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

bolov 09.05.2019 11:45

@bolov Компилятору разрешено предполагать, что это правда, но это не обязательно, поэтому вы не можете.

user253751 09.05.2019 23:46

@immibis нет, это требуется по стандарту. Точно так же, как стандарт требует, чтобы разыменованный указатель не был нулевым.

bolov 10.05.2019 00:48

@bolov Правда? Компилятор требуется предполагает, что i + 10 > i верно? Итак, int i = INT_MAX; if (i + 10 > i) printf("hello!\n");требуется печатает "hello!\n" и не может форматировать мой жесткий диск?

user253751 10.05.2019 01:31

@immibis, ты не понимаешь. Да!! компилятор должен предположить, что i + 10 > i, поэтому он будет генерировать код, который действителен только при i < INT_MAX - 10. В этом случае i >= INT_MAX код недействителен, стандарт не предписывает какое-либо поведение, и компилятор может сгенерировать код, исходя из предположения i < INT_MAX. Для недопустимого кода - неопределенное поведение может произойти что угодно, потому что компилятору не требуется генерировать какое-либо поведение.

bolov 10.05.2019 02:07

@bolov В этом случае также разрешено генерировать код, где i + 10 > i оценивается как false.

user253751 10.05.2019 02:11

@immibis, честно говоря, это как со стеной разговаривать

bolov 10.05.2019 02:12

Я точно знаю? «С UB вы получаете целые числа со знаком C/C++, которые ведут себя арифметически так, как вы ожидаете математически». довольно четко определяет UB.

user253751 10.05.2019 02:13

@immibis, ты видишь, как компилятор превращает x + 10 >= x; в mov eax, 1? Это именно потому, что он может предположить, что x + 10 >= x всегда будет правдой (и я повторяю это снова в последний раз: для действительного кода)

bolov 10.05.2019 02:26

Компилятор может это предположить. Программист не может.

user253751 10.05.2019 02:31

@immibis ... ты троллишь? 1-й: когда мы говорили о программисте? Все обсуждение, весь пост и комментарии касаются того, что может предположить компилятор (и что, следовательно, может оптимизировать). 2-й также программист может предположить, что на действительном коде x + 10 >= x. .........

bolov 10.05.2019 02:36

@bolov Программист должен знать, будет ли x + 10 >= x истинным для компилятора, который определил поведение переполнения, чтобы знать, действителен ли код. Они не могут рассуждать «этот код действителен, поэтому я знаю x <= INT_MAX+10». Они должны рассуждать «x <= INT_MAX+10 из-за чего-то другого, поэтому этот код действителен».

user253751 10.05.2019 02:40

@immibis, сейчас ты делаешь это нарочно. Вы уводите дискуссию от исходной проблемы. Мое утверждение таково: «компилятор будет предполагать, что для действительного кода x + 10 >= x всегда будет истинным — это предписано стандартом — поэтому компилятор может оптимизировать выражение до true». Это всегда было моим утверждением. Это то, что я обсуждаю (и то, что вы оспаривали изначально). Вы хотите сейчас поговорить о том, как программист должен убедиться, что он не пишет код, который имеет UB и, следовательно, недействителен, это другой разговор.

bolov 10.05.2019 02:46

Вы не сказали "компилятор предположит, что..." - вы просто сказали "...", что является констатацией факта, а не констатацией предположения, что, если оно неверно, все сломается.

user253751 10.05.2019 03:01

Кажется, я наконец понял вашу точку зрения. Да, я не сказал "компилятор" и изначально не сказал "для корректного кода". Однако я думал, что это понятно, так как речь идет исключительно о том, что компилятор может сделать с данным кодом. Теперь мне кажется, что вы взяли «x + 1 >= 1 всегда будет истинным» как утверждение, означающее «всякий раз, когда вы видите x + 1 >= 1 в коде C, вы, программист, можете быть уверены, что это всегда будет истинным». Это никогда не входило в мои намерения, и я думаю, что из контекста поста становится ясно, о чем я говорю. Прошу прощения за предположение о злом умысле.

bolov 10.05.2019 03:14

«Троллинг» и разговоры друг с другом часто ужасно похожи друг на друга.

curiousguy 11.05.2019 16:54

Существует множество полезных преобразований, которые можно упростить, объявив, что целочисленное переполнение является UB, но также можно упростить, указав, что некоторые аспекты поведения являются Unspecified. Существует разница между разрешением компилятору обрабатывать x+y > z как произвольное получение 0 или 1 без побочных эффектов в случае переполнения и разрешением произвольно разрушительных побочных эффектов. Если единственными ситуациями, в которых имеет значение, возвращает ли функция 0 или 1, являются те, в которых переполнение не происходит, обработка переполнения в этом выражении как произвольное получение 0 или 1...

supercat 16.02.2022 21:31

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

supercat 16.02.2022 21:32

Вот реальный маленький эталон, пузырьковая сортировка. Я сравнил тайминги без/с -fwrapv (что означает переполнение UB/не UB). Вот результаты (секунды):

                   -O3     -O3 -fwrapv    -O1     -O1 -fwrapv
Machine1, clang    5.2     6.3            6.8     7.7
Machine2, clang-8  4.2     7.8            6.4     6.7
Machine2, gcc-8    6.6     7.4            6.5     6.5

Как видите, версия без UB (-fwrapv) почти всегда медленнее, самая большая разница довольно большая, 1,85x.

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

#include <stdio.h>
#include <stdlib.h>

void bubbleSort(int *a, long n) {
        bool swapped;
        for (int i = 0; i < n-1; i++) {
                swapped = false;
                for (int j = 0; j < n-i-1; j++) {
                        if (a[j] > a[j+1]) {
                                int t = a[j];
                                a[j] = a[j+1];
                                a[j+1] = t;
                                swapped = true;
                        }
                }

                if (!swapped) break;
        }
}

int main() {
        int a[8192];

        for (int j=0; j<100; j++) {
                for (int i=0; i<8192; i++) {
                        a[i] = rand();
                }

                bubbleSort(a, 8192);
        }
}

Другие вопросы по теме