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

В стандарте 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: John Regehr «Закрытие Keynote : Неопределенное поведение и оптимизация компилятора »

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


person chqrlie    schedule 08.05.2019    source источник
comment
Комментарии не подлежат расширенному обсуждению; этот разговор был перешел в чат.   -  person Samuel Liew♦    schedule 09.05.2019
comment
Причина, по которой C говорит, что знаковое целочисленное переполнение не определено, заключается в том, что некоторые процессоры используют дополнение 2, некоторые используют дополнение 1, некоторые используют знак и величину; и во всех случаях переполнение может вызвать что угодно (например, в процессорах типа MIPS есть ловушка при переполнении). Другими словами, речь идет о переносимости, а не об оптимизации.   -  person Brendan    schedule 09.05.2019
comment
Точно. Единственная «значимая статистика», которая кому-то нужна, - это то, что существуют компьютеры с дополнением до единиц и величиной знака.   -  person user207421    schedule 09.05.2019
comment
@ user207421: Да, это хороший вопрос, ответ на который, кажется, больше не. Отсюда текущее предложение об удалении поддержки представлений с дополнением, отличным от двух: open-std.org/jtc1/sc22/wg14/www/docs/n2330.pdf   -  person chqrlie    schedule 09.05.2019
comment
@Brendan: архитектура дополнений уходит в прошлое. Можно выбрать ловушку MIPS при переполнении.   -  person chqrlie    schedule 09.05.2019
comment
@chqrlie: C тоже из прошлого (сейчас 47 лет). Было много дизайнерских решений, которые тогда имели смысл, которые больше не имеют смысла, но продолжают существовать, потому что изменения могут сломать слишком много существующего программного обеспечения.   -  person Brendan    schedule 09.05.2019


Ответы (4)


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

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

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

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

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

Упрощение знаковых целочисленных выражений

  • # P6 #
    (x * c) cmp 0   ->   x cmp 0 
    
    bool foo(int x) { return x * 10 > 0 }
    
    foo(int):
            test    edi, edi
            setg    al
            ret
    
  • # P7 #
    # P8 #
    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
    
  • # P12 #
    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
    

    Второе преобразование допустимо, только если c1 ‹= c2, так как в противном случае оно привело бы к переполнению, когда y имеет значение INT_MIN.

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

Указатель арифметики и продвижение шрифта

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

Еще один аспект этого заключается в том, что неопределенное переполнение гарантирует, что a [i] и a [i + 1] являются смежными. Это улучшает анализ обращений к памяти для векторизации и т. Д.

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

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

Версия без подписи

#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

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

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

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

он определяет, что x имеет диапазон [1, INT_MAX] после оператора if, и, таким образом, может определить, что y имеет диапазон [6, INT_MAX], поскольку переполнение не допускается. А следующую строку можно оптимизировать до int z = y >> 2;, поскольку компилятор знает, что y неотрицательно.

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

Неопределенное переполнение помогает оптимизации, которые нуждаются в сравнении двух значений (поскольку случай упаковки даст возможные значения формы [INT_MIN, (INT_MIN+4)] или [6, INT_MAX], что предотвращает все полезные сравнения с < или >), например

  • Изменение сравнения x<y на истину или ложь, если диапазоны для x и y не перекрываются
  • Изменение min(x,y) или max(x,y) на x или y, если диапазоны не перекрываются
  • Изменение abs(x) на x или -x, если диапазон не выходит за 0
  • Изменение x/c на x>>log2(c), если x>0 и константа c является степенью 2
  • Изменение x%c на x&(c-1), если x>0 и константа c является степенью 2

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

Канонический пример того, почему неопределенное подписанное переполнение помогает оптимизации цикла, заключается в том, что такие циклы, как

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

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

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

person bolov    schedule 09.05.2019
comment
x + 10 ›x верно сейчас - нет, это не так. Он может отформатировать ваш жесткий диск, если x окажется равным INT_MAX-5. - person user253751; 09.05.2019
comment
Очень информативный ответ (блог действительно хорошо читают). Поскольку все эти оптимизации возможны только для операндов со знаком, вывод противоречит интуиции: использование значений индекса size_t для многих вычислений должно привести к менее эффективному коду из-за семантики арифметики по модулю. Это хороший аргумент для включения ssize_t в стандарт C или какого-либо беззнакового типа без арифметики по модулю. - person chqrlie; 09.05.2019
comment
очаровательный. хорошая находка. - person Richard Hodges; 09.05.2019
comment
@immibis Я имел в виду, что это верно для всего действительного кода. UB недействительный код. - person bolov; 09.05.2019
comment
@chqrlie Я слышал на cppcon, что видные члены комитета по стандартизации признают, что размер без знака был ошибкой в ​​ретроспективе. То же самое и с циклическим поведением unsigned (для обычных типов лучше было бы иметь UB при переполнении и иметь отдельные типы с циклическим поведением, когда вам это явно нужно) - person bolov; 09.05.2019
comment
@bolov Компилятору разрешено предполагать, что это правда, но это не обязательно, поэтому вы не можете. - person user253751; 10.05.2019
comment
@immib - нет, этого требует стандарт. Точно так же, как стандарт требует предполагать, что разыменованный указатель не равен нулю. - person bolov; 10.05.2019
comment
@bolov В самом деле? От компилятора требуется предположить, что i + 10 > i истинно? Итак, int i = INT_MAX; if(i + 10 > i) printf("hello!\n"); требуется для печати hello! \ N и не разрешено форматировать мой жесткий диск? - person user253751; 10.05.2019
comment
@immibis, вы этого не понимаете. Да!! компилятор должен предполагать, что i + 10 > i, поэтому он будет генерировать код, действительный только тогда, когда i < INT_MAX - 10. Для этого i >= INT_MAX код недействителен, стандарт не требует какого-либо поведения, и компилятор может сгенерировать код, сделав предположение i < INT_MAX. При недопустимом коде - неопределенное поведение может произойти все, что угодно, потому что компилятору не требуется генерировать какое-либо поведение. - person bolov; 10.05.2019
comment
@bolov Также разрешено сгенерировать код, где i + 10 > i в этом случае принимает значение false. - person user253751; 10.05.2019
comment
@immibis, честно говоря, это как говорить со стеной - person bolov; 10.05.2019
comment
Я точно знаю? С UB вы получаете целые числа со знаком C / C ++, которые ведут себя арифметически так, как вы ожидаете математически. довольно четко определяет UB. - person user253751; 10.05.2019
comment
@immibis вы видите, как компилятор преобразует x + 10 >= x; в mov eax, 1? Это именно потому, что он может предполагать, что x + 10 >= x всегда будет истинным (и я повторяю это еще раз в последний раз: для действительного кода) - person bolov; 10.05.2019
comment
Компилятор может это предположить. Программист не может. - person user253751; 10.05.2019
comment
@immibis ... ты троллинг? 1-й: когда мы говорили о программисте? Все обсуждения, весь пост и комментарии касаются того, что может предполагать компилятор (и что в результате может оптимизировать). 2-й также программист может предположить, что по валидному коду x + 10 >= x. ......... - person bolov; 10.05.2019
comment
@bolov Программист должен знать, будет ли x + 10 >= x истинным для компилятора, который определил поведение переполнения, чтобы знать, действителен ли код. Они не могут обосновать, что этот код действителен, поэтому я знаю x <= INT_MAX+10. Они должны рассуждать x <= INT_MAX+10 из-за чего-то еще, поэтому этот код действителен. - person user253751; 10.05.2019
comment
@immibis, сейчас вы делаете это специально. Вы уводите обсуждение от исходной проблемы. Мое утверждение состоит в том, что компилятор предполагает, что для действительного кода x + 10 >= x всегда будет истинным - это предусмотрено стандартом, - поэтому компилятор может оптимизировать выражение до true. Это всегда было моим заявлением. Это то, что я обсуждаю (и то, что вы оспаривали изначально). Теперь вы хотите поговорить о том, как программист должен убедиться, что он не пишет код с UB и, следовательно, недопустимый, это другое обсуждение. - person bolov; 10.05.2019
comment
Вы не сказали, что компилятор будет предполагать, что ... - вы просто сказали ... что является констатацией факта, а не утверждением предположения, что, если оно не истинно, что-то сломает. - person user253751; 10.05.2019
comment
Думаю, я наконец понял вашу точку зрения. Да, я не сказал "компилятор" и изначально не сказал "правильный код". Однако я подумал, что это понятно, поскольку мы говорим исключительно о том, что компилятор может делать с данным кодом. Мне теперь кажется, что вы взяли x + 1 ›= 1 всегда будет истинным как утверждение, означающее, что всякий раз, когда вы видите x + 1 >= 1 в коде C, вы, программист, можете быть уверены, что оно всегда будет истинным. Это никогда не входило в мои намерения, и я думаю, что из контекста сообщения ясно, о чем я говорю. Прошу прощения за предположение о злых умыслах. - person bolov; 10.05.2019
comment
Троллинг и разговоры друг с другом часто ужасно похожи друг на друга. - person curiousguy; 11.05.2019

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

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

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

person anatolyg    schedule 08.05.2019
comment
да. Создание неверного значения - серьезная проблема, а правильно выглядящее значение еще более проблематично. Во многих случаях результат не ожидается, поэтому к любому напечатанному значению можно отнестись серьезно. Это могло даже вызвать уязвимости в системе безопасности. - person curiousguy; 13.05.2019

Вот настоящий небольшой тест, пузырьковая сортировка. Я сравнил тайминги без / с -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);
        }
}
person geza    schedule 09.05.2019

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

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

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

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

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

Кроме того, как уже было сказано, указание того, что подписанное переполнение должно быть 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:

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

person Richard Hodges    schedule 09.05.2019
comment
Ваш конкретный способ дополнения 2 имеет другую семантику: он вернет INT_MAX для отрицательных значений a меньше -b. - person chqrlie; 09.05.2019
comment
@chqrlie Я, должно быть, неправильно понял требования функции. Я предположил, что, поскольку мы ограничиваемся положительными числами, входные данные гарантированно будут положительными. - person Richard Hodges; 09.05.2019
comment
В этом глупом примере функция проверяет только положительное переполнение, потому что b по своей природе положительно, но a может иметь отрицательное значение. Это всего лишь пример, и ваша sum_max версия - правильный способ достижения цели переносимым способом, но я видел, как устаревший код использовал опубликованный мною тест, который никогда не был правильным, но просто работал до тех пор, пока около 10 лет назад. - person chqrlie; 09.05.2019
comment
Кстати, C ++ недавно удалил альтернативные представления целых чисел со знаком. Дополнение two теперь является единственным поддерживаемым. - person chqrlie; 09.05.2019
comment
@chqrlie Это верно для C ++ 20, но я не упоминал об этом, потому что переполнение целых чисел со знаком остается UB по всем ранее упомянутым причинам оптимизации. Я не хотел создавать ложное впечатление, что старый код станет нормальным. Не будет. - person Richard Hodges; 09.05.2019