Сообщение Re[11]: Откуда эта лютая любовь к знаковым целым? от 07.05.2020 17:52
Изменено 07.05.2020 17:54 netch80
Re[11]: Откуда эта лютая любовь к знаковым целым?
Здравствуйте, Reset, Вы писали:
R>Не очень понимаю, про какие проверки вы все говорите: godbolt (смотреть на серенькое и желтенькое, т.е. содержимое цикла).
Вы не на то смотрите, посмотрите на прошлый пример. В беззнаковом варианте вычитание и сразу je, в знаковом вычитание, test для проверки результата и за ним jle.
У нас есть цикл: for(TYPE i = 0; i < (end — begin); ++i ) { baz(i); } // (в каком примере где foo, bar, baz — мне пофиг, разберётесь по описанию)
Сначала компилятор его разбирает в:
а затем — типовая оптимизация сейчас — превращает в:
таким образом, условие выхода в этом варианте может быть реализовано дважды, с противоположным финальным знаком (в условии невхода — инвертировано, а продолжения — прямое).
Почему такая оптимизация — с ходу не помню — но, как вариант, из-за branch prediction.
Теперь начинаем оптимизировать. Компилятор знает, что в цикл он вошёл при i=0, затем i только увеличивался, и он может выкинуть в условии продолжения варианты i >= (end-begin) — что он успешно и делает; поэтому в цикле независимо от знака будет проверка одного и того же типа — на границу, зная, что мы к ней подходим снизу. А вот вначале — на входной проверке прямо перед cycle_body — он в случае знакового знает, что 0 может оказаться всё равно не меньше, чем end — begin, ему не гарантировали ничего про эту разность, поэтому вставлены test + jle; а при беззнаковых 0 > end — begin не может быть, поэтому он сокращает >= до ==.
В вашем же примере картина замутнена использованием не long (тип, равный ptrdiff_t и size_t по ширине), а int. Поэтому мышление компилятора сбито уже странными эффектами — например, что, если end — begin переполнит даже uint32_t? Отсюда совершенно ненужный, в норме, jle на входной проверке. Более того, там jl в цикле! Вы совсем запутали бедную программу, она если на вашем unsigned достигнет 2**31... нет, выполнение не нарушится за счёт того, что `mov eax, ebx` косвенно беззнаково расширяет 32->64, но jl вместо jne показывает, что ему поплохело. А вот если signed int, то он делает jne!
Не делайте так, это вредно.
R> IMHO, разница между int и unsigned в том, что unsigned — это целое по модулю 2^32 и при переполнении он должен оставаться 32-х битным, поэтому компилятор работает с unsigned в отдельном 32-х битном регистре и там появляется лишняя команда (на самом деле регистр тот же, но приходится из 32-х бит делать 64, как результат — лишняя команда). Для int переполнение — это UB, поэтому он использует сразу 64-х битный регистр, который будет вести себя как 32-х битный, если нет переполнения. Поэтому дополнительной команды там нет, ну, и в случае переполнения UB проявится в том, что будет поведение не как у 32-х битного числа.
R>Не очень понимаю, про какие проверки вы все говорите: godbolt (смотреть на серенькое и желтенькое, т.е. содержимое цикла).
Вы не на то смотрите, посмотрите на прошлый пример. В беззнаковом варианте вычитание и сразу je, в знаковом вычитание, test для проверки результата и за ним jle.
У нас есть цикл: for(TYPE i = 0; i < (end — begin); ++i ) { baz(i); } // (в каком примере где foo, bar, baz — мне пофиг, разберётесь по описанию)
Сначала компилятор его разбирает в:
i = 0;
cycle_body_start: if (!(i < (end - begin))) goto cycle_break;
baz(i);
cycle_continue: ++i; goto cycle_body_start;
cycle_break:
а затем — типовая оптимизация сейчас — превращает в:
i = 0;
if (!(i < (end - begin))) goto cycle_break;
cycle_body:
baz(i);
cycle_continue: ++i;
if (i < (end - begin)) goto cycle_body;
cycle_break:
таким образом, условие выхода в этом варианте может быть реализовано дважды, с противоположным финальным знаком (в условии невхода — инвертировано, а продолжения — прямое).
Почему такая оптимизация — с ходу не помню — но, как вариант, из-за branch prediction.
Теперь начинаем оптимизировать. Компилятор знает, что в цикл он вошёл при i=0, затем i только увеличивался, и он может выкинуть в условии продолжения варианты i >= (end-begin) — что он успешно и делает; поэтому в цикле независимо от знака будет проверка одного и того же типа — на границу, зная, что мы к ней подходим снизу. А вот вначале — на входной проверке прямо перед cycle_body — он в случае знакового знает, что 0 может оказаться всё равно не меньше, чем end — begin, ему не гарантировали ничего про эту разность, поэтому вставлены test + jle; а при беззнаковых 0 > end — begin не может быть, поэтому он сокращает >= до ==.
В вашем же примере картина замутнена использованием не long (тип, равный ptrdiff_t и size_t по ширине), а int. Поэтому мышление компилятора сбито уже странными эффектами — например, что, если end — begin переполнит даже uint32_t? Отсюда совершенно ненужный, в норме, jle на входной проверке. Более того, там jl в цикле! Вы совсем запутали бедную программу, она если на вашем unsigned достигнет 2**31... нет, выполнение не нарушится за счёт того, что `mov eax, ebx` косвенно беззнаково расширяет 32->64, но jl вместо jne показывает, что ему поплохело. А вот если signed int, то он делает jne!
Не делайте так, это вредно.
R> IMHO, разница между int и unsigned в том, что unsigned — это целое по модулю 2^32 и при переполнении он должен оставаться 32-х битным, поэтому компилятор работает с unsigned в отдельном 32-х битном регистре и там появляется лишняя команда (на самом деле регистр тот же, но приходится из 32-х бит делать 64, как результат — лишняя команда). Для int переполнение — это UB, поэтому он использует сразу 64-х битный регистр, который будет вести себя как 32-х битный, если нет переполнения. Поэтому дополнительной команды там нет, ну, и в случае переполнения UB проявится в том, что будет поведение не как у 32-х битного числа.
Re[11]: Откуда эта лютая любовь к знаковым целым?
Здравствуйте, Reset, Вы писали:
R>Не очень понимаю, про какие проверки вы все говорите: godbolt (смотреть на серенькое и желтенькое, т.е. содержимое цикла).
Вы не на то смотрите, посмотрите на прошлый пример. В беззнаковом варианте вычитание и сразу je, в знаковом вычитание, test для проверки результата и за ним jle.
У нас есть цикл: for(TYPE i = 0; i < (end — begin); ++i ) { baz(i); } // (в каком примере где foo, bar, baz — мне пофиг, разберётесь по описанию)
Сначала компилятор его разбирает в:
а затем — типовая оптимизация сейчас — превращает в:
таким образом, условие выхода в этом варианте может быть реализовано дважды, с противоположным финальным знаком (в условии невхода — инвертировано, а продолжения — прямое).
Почему такая оптимизация — с ходу не помню — но, как вариант, из-за branch prediction.
Теперь начинаем оптимизировать. Компилятор знает, что в цикл он вошёл при i=0, затем i только увеличивался, и он может выкинуть в условии продолжения варианты i >= (end-begin) — что он успешно и делает; поэтому в цикле независимо от знака будет проверка одного и того же типа — на границу, зная, что мы к ней подходим снизу. А вот вначале — на входной проверке прямо перед cycle_body — он в случае знакового знает, что 0 может оказаться всё равно не меньше, чем end — begin, ему не гарантировали ничего про эту разность, поэтому вставлены test + jle; а при беззнаковых 0 > end — begin не может быть, поэтому он сокращает >= до ==.
В вашем же примере картина замутнена использованием не long (тип, равный ptrdiff_t и size_t по ширине), а int. Поэтому мышление компилятора сбито уже странными эффектами — например, что, если end — begin переполнит даже uint32_t? Отсюда совершенно ненужный, в норме, jle на входной проверке. Более того, там jl в цикле! Вы совсем запутали бедную программу, она если на вашем unsigned достигнет 2**31... нет, выполнение не нарушится за счёт того, что `mov eax, ebx` косвенно беззнаково расширяет 32->64, но jl вместо jne показывает, что ему поплохело. А вот если signed int, то он делает jne!
Не делайте так, это вредно.
R>Не очень понимаю, про какие проверки вы все говорите: godbolt (смотреть на серенькое и желтенькое, т.е. содержимое цикла).
Вы не на то смотрите, посмотрите на прошлый пример. В беззнаковом варианте вычитание и сразу je, в знаковом вычитание, test для проверки результата и за ним jle.
У нас есть цикл: for(TYPE i = 0; i < (end — begin); ++i ) { baz(i); } // (в каком примере где foo, bar, baz — мне пофиг, разберётесь по описанию)
Сначала компилятор его разбирает в:
i = 0;
cycle_body_start: if (!(i < (end - begin))) goto cycle_break;
baz(i);
cycle_continue: ++i; goto cycle_body_start;
cycle_break:
а затем — типовая оптимизация сейчас — превращает в:
i = 0;
if (!(i < (end - begin))) goto cycle_break;
cycle_body:
baz(i);
cycle_continue: ++i;
if (i < (end - begin)) goto cycle_body;
cycle_break:
таким образом, условие выхода в этом варианте может быть реализовано дважды, с противоположным финальным знаком (в условии невхода — инвертировано, а продолжения — прямое).
Почему такая оптимизация — с ходу не помню — но, как вариант, из-за branch prediction.
Теперь начинаем оптимизировать. Компилятор знает, что в цикл он вошёл при i=0, затем i только увеличивался, и он может выкинуть в условии продолжения варианты i >= (end-begin) — что он успешно и делает; поэтому в цикле независимо от знака будет проверка одного и того же типа — на границу, зная, что мы к ней подходим снизу. А вот вначале — на входной проверке прямо перед cycle_body — он в случае знакового знает, что 0 может оказаться всё равно не меньше, чем end — begin, ему не гарантировали ничего про эту разность, поэтому вставлены test + jle; а при беззнаковых 0 > end — begin не может быть, поэтому он сокращает >= до ==.
В вашем же примере картина замутнена использованием не long (тип, равный ptrdiff_t и size_t по ширине), а int. Поэтому мышление компилятора сбито уже странными эффектами — например, что, если end — begin переполнит даже uint32_t? Отсюда совершенно ненужный, в норме, jle на входной проверке. Более того, там jl в цикле! Вы совсем запутали бедную программу, она если на вашем unsigned достигнет 2**31... нет, выполнение не нарушится за счёт того, что `mov eax, ebx` косвенно беззнаково расширяет 32->64, но jl вместо jne показывает, что ему поплохело. А вот если signed int, то он делает jne!
Не делайте так, это вредно.