Re[8]: Проверка выполнения сложения
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 13.04.12 10:47
Оценка:
Здравствуйте, rg45, Вы писали:

N>>Это я подпилил на сравнение по методу rg45 вместо прежней конверсии к int64 и выходу за границы:


R>Гм, а зачем приводить слагаемые к __int32?


Так в оригинале. Это код конкретно для случая template < typename T, typename U > class AdditionHelper < T, U, AdditionState_CastInt64CheckMinMax >, который назначен вызываться для сложения двух SafeInt<int>. По-нормальному надо было бы породить отдельный шаблонный класс, но мне было облом.

R> А что будет в случае, если T — беззнаковый 32-битный тип?


В данном конкретном SafeInt3.hpp вызовётся метод другого шаблонного класса, который я не трогал.

R> В результате интерпретации беззнакового типа как знакового возможны две ошибочные ситуации:


Это очевидно, но не мой случай.
The God is real, unless declared integer.
Re[2]: Проверка выполнения сложения
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 13.04.12 19:45
Оценка: 1 (1)
Здравствуйте, rg45, Вы писали:

R>Как-то так:

[...]
R>Будет работать и для знаковых и для беззнаковых.

Всё однако хитрее. Может, я поспешил с похвалой?

Clang 3.0 на 32-битке при -O (и выше) твоё выражение под assert'ом считает истинным *всегда*, соответственно проверка не происходит.
Это независимо от того, используется ли '>' или '>=' в обоих сравнениях.

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

На 64-битке такой проблемы нет, проверки честно производятся.

Проверочная программа, чуть более обширная.

Первый файл (разделено, чтобы не инлайнило):

extern void p(int a, int b);

int main() {
  p(1, 1);
  p(0x30000000, 0x30000000);
  p(0x40000000, 0x40000000);
  p(0x50000000, 0x50000000);
  p(0xB0000000, 0xB0000000);
  p(0xC0000000, 0xC0000000);
  p(0xD0000000, 0xD0000000);
  p(-1, -1);
  return 0;
}

Второй файл:
#include <stdio.h>
#include <limits.h>

void p(int a, int b)
{
  int result = a + b;
  int x1 = a ^ b;
  int ovf1, ovf2, ovf3, ovf4;
  if (x1 >= 0)
    ovf1 = ((a ^ result) < 0);
  else
    ovf1 = 0;
  if (b >= 0)
    ovf2 = (a > INT_MAX - b);
  else
    ovf2 = (a < INT_MIN - b);
  ovf3 = ((b >= 0) != (result >= a));
  ovf4 = ((b > 0) != (result > a));
  printf("%8x %8x %8x %1d:%1d:%1d:%1d\n", a, b, result,
      ovf1, ovf2, ovf3, ovf4);
}


Результат под gcc везде, под open64 (он только на 64 бывает), под clang на 32 битах без оптимизации, и под clang на 64 битах:

       1        1        2 0:0:0:0
30000000 30000000 60000000 0:0:0:0
40000000 40000000 80000000 1:1:1:1
50000000 50000000 a0000000 1:1:1:1
b0000000 b0000000 60000000 1:1:1:1
c0000000 c0000000 80000000 0:0:0:0
d0000000 d0000000 a0000000 0:0:0:0
ffffffff ffffffff fffffffe 0:0:0:0


Результат на clang в 32 битах с -O:
       1        1        2 0:0:0:0
30000000 30000000 60000000 0:0:0:0
40000000 40000000 80000000 1:1:0:0
50000000 50000000 a0000000 1:1:0:0
b0000000 b0000000 60000000 1:1:0:0
c0000000 c0000000 80000000 0:0:0:0
d0000000 d0000000 a0000000 0:0:0:0
ffffffff ffffffff fffffffe 0:0:0:0


Заодно померял скорость: вариант через xor'ы на ~4% медленнее варианта с двумя сравнениями.

Надо бы подсказку от гуру стандартов.
The God is real, unless declared integer.
Re[3]: Проверка выполнения сложения
От: rg45 СССР  
Дата: 13.04.12 20:01
Оценка:
Здравствуйте, netch80, Вы писали:

N>Всё однако хитрее. Может, я поспешил с похвалой?


Каждая похвала на учете?


N>Clang 3.0 на 32-битке при -O (и выше) твоё выражение под assert'ом считает истинным *всегда*, соответственно проверка не происходит.

N>Это независимо от того, используется ли '>' или '>=' в обоих сравнениях.

Забавно. Но имеет право, поскольку, в отсутствие переполнения это выражение действительно всегда истинно, а при переполнении компилятор с оптимизатором делают что хотят, потому как UB
--
Справедливость выше закона. А человечность выше справедливости.
Re[4]: Проверка выполнения сложения
От: rg45 СССР  
Дата: 13.04.12 20:27
Оценка:
Здравствуйте, rg45, Вы писали:

N>>Clang 3.0 на 32-битке при -O (и выше) твоё выражение под assert'ом считает истинным *всегда*, соответственно проверка не происходит.

N>>Это независимо от того, используется ли '>' или '>=' в обоих сравнениях.

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


А что если попробовать обмануть оптимизатор?

// Определение этой константы вынесено
// в отдельную единицу трансляции, чтобы
// оптимизатор не видел ее значения.
extern const unsigned char zero;

template<typename T>
T add (T a, T b)
{
  T c = a + b;
  assert(b >= zero == c >= a);
  return c;  
}


Конечно, это уже не так заманчиво, как выглядело раньше.
--
Справедливость выше закона. А человечность выше справедливости.
Re[5]: Проверка выполнения сложения
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 14.04.12 06:48
Оценка: 7 (1)
Здравствуйте, rg45, Вы писали:

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


R>А что если попробовать обмануть оптимизатор?


Не помогает Но очень своеобразно. Вот сишный код и разборка ассемблера.

extern const int foobar;
int ovf3(int a, int b)
{
  int result = a + b;
  return ((b >= foobar) != (result >= a));
}


Получаем:
000000a0 <ovf3>:
  a0:   8b 44 24 08             mov    0x8(%esp),%eax ; это прочли b
  a4:   89 c1                   mov    %eax,%ecx      ; скопировали в %ecx
  a6:   c1 e9 1f                shr    $0x1f,%ecx     ; оставили от него один знак (то есть b<0)
  a9:   39 05 00 00 00 00       cmp    %eax,0x0       ; внешняя переменная foobar, адрес пока неизвестен
  af:   0f 9e c0                setle  %al            ; al - результат (b<foobar)
  b2:   0f b6 c0                movzbl %al,%eax       ; растянули его до полного %eax
  b5:   31 c8                   xor    %ecx,%eax      ; неравенство двух тестов, то есть (b<0) != (b<foobar)
  b7:   83 f0 01                xor    $0x1,%eax      ; булевская инверсия - превратили в равенство тестов
  ba:   c3                      ret


Получается, что clang из (result >= a) сделал ((a+b)>=a), это очевидно упростил до (b>=0), ну а дальше очевидно, что проверка всегда даст (b>=0) == (b>=foobar), если foobar == 0

Так что "в живых" остались сравнение по XOR и по двум границам сравнения с пределами.
The God is real, unless declared integer.
Re[4]: Проверка выполнения сложения
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 14.04.12 07:02
Оценка:
Здравствуйте, rg45, Вы писали:

N>>Всё однако хитрее. Может, я поспешил с похвалой?

R>Каждая похвала на учете?

Нет, но если похвалил решение (не человека), а дальше нашлись проблемы с этим решением — то что ещё сказать?

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


Вот честно жаль, что нету поддержки в библиотеке.
Какие-то несчастные div() есть, а поддержки простейшего сложения с детектом переполнения — нет.
The God is real, unless declared integer.
Re[6]: Проверка выполнения сложения
От: rg45 СССР  
Дата: 14.04.12 08:55
Оценка:
Здравствуйте, netch80, Вы писали:

N>...

N>Получается, что clang из (result >= a) сделал ((a+b)>=a), это очевидно упростил до (b>=0), ну а дальше очевидно, что проверка всегда даст (b>=0) == (b>=foobar), если foobar == 0

Мда, жестокий оптимизатор, ничего не скажешь. Выходит, что в самом общем случае, использовать результат сложения вообще нельзя, поскольку в случае переполнения его значение может оказаться любым, и кто знает, каким образом этот факт будет учтен тем или другим компилятором. В общем, UB — оно и в Африке UB. Кстати, вариант с xor тоже использует результат сложения, поэтому он также чреват.
--
Справедливость выше закона. А человечность выше справедливости.
Re[7]: Проверка выполнения сложения
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 14.04.12 12:50
Оценка:
Здравствуйте, rg45, Вы писали:

N>>...

N>>Получается, что clang из (result >= a) сделал ((a+b)>=a), это очевидно упростил до (b>=0), ну а дальше очевидно, что проверка всегда даст (b>=0) == (b>=foobar), если foobar == 0
R>Мда, жестокий оптимизатор, ничего не скажешь. Выходит, что в самом общем случае, использовать результат сложения вообще нельзя, поскольку в случае переполнения его значение может оказаться любым, и кто знает, каким образом этот факт будет учтен тем или другим компилятором.

Так-то оно так, но на практике я пока не видел реализаций, где не выполнялось бы правило, что сумма двух чисел размером N (после штатного расширения, например, short -> int), *приведённая к тому же типу* (этот момент тут принципиален), не давала бы N младших бит суммы. Но на практике сейчас невероятно встретить реализацию иную, чем "дополнение до 2", поэтому неудивительно, что оно так.

R> В общем, UB — оно и в Африке UB. Кстати, вариант с xor тоже использует результат сложения, поэтому он также чреват.


Да, но вероятность этой чреватости даже не в разы, а на порядки ниже.
Пока что я не вижу потенциального UB только у двух не-ассемблерных реализаций — с контр-вычитанием (ovf2 в прошлом примере) и с расширением разрядности (как в SafeInt<>), но последняя значительно дороже для всего шириной в int и выше.

Кстати, мы пока что обсуждали только сложение, но есть ещё тема умножения. И вот с ним всё мрачнее — я пока не представляю себе иных вариантов, кроме как ассемблер или проверка на расширенной разрядности.
The God is real, unless declared integer.
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.