Информация об изменениях

Сообщение Re[24]: Carbon от 21.04.2024 18:30

Изменено 21.04.2024 19:19 Sinclair

Re[24]: Carbon
Здравствуйте, kov_serg, Вы писали:

_>Здравствуйте, Sinclair, Вы писали:


S>>Давайте попробуем починить UB в функции, написанной просто и строго по делу, безо всяких шаблонов:

S>>
S>>long avg(long a, long b, long c)
S>>{
S>>  return (a+b+c)/3; 
S>>}
S>>

_>Переполнение можно убрать, заменив одни UB на другие
_>
_>long avg(long a,long b,long c) {
_>    long r1=(a>>2)+(b>>2)+(c>>2), r2=r1%3; 
_>    r1-=r2; r2=4*r2+(a&3)+(b&3)+(c&3);
_>    if (r1<0 && r2>0) { r2-=12; r1++; }
_>    r1/=3; r2/=3; return (r1<<2)+r2;
_>}
_>

Неплохо, неплохо. А зачем вы пользуетесь implementation-defined поведением? Вы же явно рассчитываете на то, что a >> 2 эквивалентно делению (на Интеле все рассмотренные на godbolt компиляторы делают это именно так; но, вообще говоря, компилятор не обязан делать арифметический сдвиг. А в случае логического сдвига будет ошибка.) Почему бы просто не поделить на 4?
Любой современный компилятор заменяет такие деления сдвигами (и на этот раз — ровно теми сдвигами, которые имеют нужную семантику, т.е. shr для unsigned, sar для signed).
До конца разобраться, что именно вы делаете, мне не удалось; но результат получается корректный
Я правильно понимаю, что вы хотели минимизировать количество делений?
Потому что очевидный вариант c семью делениями такой:
long avg(long a, long b, long c)
{
  long ar = a % 3, br = b % 3, cr = b % 3;
  return a / 3 + b / 3 + c / 3 + (ar+br+cr) / 3
}

Можно попытаться заставить компилятор сэкономить такты, скормив ему нарошные divrem функции:
long avg3(long a,long b,long c) {
    auto ad = std::ldiv(a, 3);
    auto bd = std::ldiv(b, 3);
    auto cd = std::ldiv(c, 3);
    return ad.quot + bd.quot + cd.quot + (ad.rem + bd.rem + cd.rem)/3;
}

Но на практике я смотрю в выхлоп кланга с -O3 — и, удивительное дело, для этого варианта он втыкает честные call ldiv@PLT, а для предыдушего — склеивает / и %, обходясь четырьмя imul-инструкциями.
Надо бы
а) побенчмаркать это дело
б) посмотреть, нельзя ли как-то всё улучшить лучший из этих результатов, опираясь на defined behavior в случае переполнений.
Re[24]: Carbon
Здравствуйте, kov_serg, Вы писали:

_>Здравствуйте, Sinclair, Вы писали:


S>>Давайте попробуем починить UB в функции, написанной просто и строго по делу, безо всяких шаблонов:

S>>
S>>long avg(long a, long b, long c)
S>>{
S>>  return (a+b+c)/3; 
S>>}
S>>

_>Переполнение можно убрать, заменив одни UB на другие
_>
_>long avg(long a,long b,long c) {
_>    long r1=(a>>2)+(b>>2)+(c>>2), r2=r1%3; 
_>    r1-=r2; r2=4*r2+(a&3)+(b&3)+(c&3);
_>    if (r1<0 && r2>0) { r2-=12; r1++; }
_>    r1/=3; r2/=3; return (r1<<2)+r2;
_>}
_>

Неплохо, неплохо. А зачем вы пользуетесь implementation-defined поведением? Вы же явно рассчитываете на то, что a >> 2 эквивалентно делению (на Интеле все рассмотренные на godbolt компиляторы делают это именно так; но, вообще говоря, компилятор не обязан делать арифметический сдвиг. А в случае логического сдвига будет ошибка.) Почему бы просто не поделить на 4?
Любой современный компилятор заменяет такие деления сдвигами (и на этот раз — ровно теми сдвигами, которые имеют нужную семантику, т.е. shr для unsigned, sar для signed).
До конца разобраться, что именно вы делаете, мне не удалось; но результат получается корректный
Я правильно понимаю, что вы хотели минимизировать количество делений?
Потому что очевидный вариант c семью делениями такой:
long avg(long a, long b, long c)
{
  long ar = a % 3, br = b % 3, cr = b % 3;
  return a / 3 + b / 3 + c / 3 + (ar+br+cr) / 3
}

Можно попытаться заставить компилятор сэкономить такты, скормив ему нарошные divrem функции:
long avg3(long a,long b,long c) {
    auto ad = std::ldiv(a, 3);
    auto bd = std::ldiv(b, 3);
    auto cd = std::ldiv(c, 3);
    return ad.quot + bd.quot + cd.quot + (ad.rem + bd.rem + cd.rem)/3;
}

Но на практике я смотрю в выхлоп кланга с -O3 — и, удивительное дело, для этого варианта он втыкает честные call ldiv@PLT, а для предыдушего — склеивает / и %, обходясь четырьмя imul-инструкциями. Подозреваю, что "экономия" тут получится отрицательная из-за накладных расходов на вызовы и связанных с ними перекладываний из регистра в регистр.
Надо бы
а) побенчмаркать это дело
б) посмотреть, нельзя ли как-то всё улучшить лучший из этих результатов, опираясь на defined behavior в случае переполнений.