Преобразование типов при вычислении

Вопросы программирования на Free Pascal, использования компилятора и утилит.

Модератор: Модераторы

Преобразование типов при вычислении

Сообщение Anika » 03.08.2020 15:56:18

Добрый день!

Занимаюсь переносом старого приложения с Delphi на Lazarus. В коде много где встречаются вычисления, в которых в качестве параметров учавствуют переменные разных типов. При вычислении и сохранении в переменную типа Cardinal приложение падает, если один из параметров byte. Падает на этапе вычисления, хотя результат не превышает допустимое значение.

Например, при таком коде приложение упадет:

Код: Выделить всё
var
ReturnData0: byte;
WholeData: Cardinal;       
begin
ReturnData0:=230;
WholeData:=ReturnData0*$1000000;

end;   


Если его переписать немного, то уже проходит нормально:
Код: Выделить всё
var
    ReturnData0: byte;
    WholeData, ReturnData01: Cardinal;
begin
ReturnData0:=230;
ReturnData01:=ReturnData0;

WholeData:=ReturnData01*$1000000;

end; 


Таких участков кода довольно много. Проверять весь код очень долго. Да и вдруг что-то пропущу, может, еще где-то такие же сюрпризы возникают.

Подозреваю, что все дело в автоматическом приведении типов при компиляции. Если так, есть ли возможность указать компилятору, чтобы все вычсиления выполнялись правильно сразу, без присваивания значения типа byte дополнительной переменной типа Cardinal?

Если мое предположение относительно автоматического приведения типов неверно, дайте, пожалуйста, подсказку, что еще влияет и как исправить малой кровью.
Anika
незнакомец
 
Сообщения: 5
Зарегистрирован: 03.08.2020 15:46:06

Re: Преобразование типов при вычислении

Сообщение pupsik » 03.08.2020 23:16:09

Например, при таком коде приложение упадет
хм.. не упало..
Версия лазаря 2.0.1, фпс 3.2.0, винда.

п.с.
Точно там копаете?
pupsik
энтузиаст
 
Сообщения: 1144
Зарегистрирован: 20.08.2014 16:20:13

Re: Преобразование типов при вычислении

Сообщение Vadim » 04.08.2020 09:43:48

Anika
Вы изначально допустили крупную ошибку - производя вычисления между переменными разных типов. У сишников в руководствах об этом специально пишут, как пример замедляющий вычисления, поскольку компилятор должен вносить дополнительный код преобразования типов. А ещё, при таком подходе, Вы можете можете получить неявную ошибку результата и просто её не заметить.

P. S. У меня Ваш код тоже не падает.
Anika писал(а):Проверять весь код очень долго.

А придётся. Потому что такой код ошибочен, что Вашими же словами и доказано - падение то не зря происходит.
Anika писал(а):Да и вдруг что-то пропущу, может, еще где-то такие же сюрпризы возникают.

А вот в этом Вы уже сами виноваты, поскольку в прошлый раз кинулись сразу писать код пропустив этап проектирования программы. ;-)
Vadim
долгожитель
 
Сообщения: 3918
Зарегистрирован: 05.10.2006 08:52:59
Откуда: Красноярск

Re: Преобразование типов при вычислении

Сообщение Anika » 04.08.2020 10:38:20

Спасибо за ответы.

Версия лазарус 2.0.6, винда, режим отладки Debug. В режиме release без ошибок.

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

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

Удалось найти варианты решения. Проблема возникает только в режиме отладки Debug. Если в параметрах отладчика режима Debug отключить в разделе "Проверки и Assert" опции проверки "Диапазон (-Cr)" и "Вызов методов (-CR)" - проблемы нет.

Но Вы правы, это плохой вариант, т.к. отключив эти опции, я могу пропустить те куски кода, где переполнение действительно возможно. Очевидно, придется дотошно проверять весь код, вводить дополнительные переменные типа Cardinal и подставлять промежуточное присваивание значения полученного byte переменной типа cardinal, и только потом выполнять вычисления.

Я предположила, что при таком вычислении происходит неявное приведение типов к Integer и это является источником проблемы при отладке. Просто надеялась, что есть возможность указать компилятору, что при вычислениях с переменными типа Cardinal нужно приведение всех переменных к этому же типу Cardinal. Вот такую опцию или инструкцию компилятору я и пыталась найти. Увы, безуспешно.
Anika
незнакомец
 
Сообщения: 5
Зарегистрирован: 03.08.2020 15:46:06

Re: Преобразование типов при вычислении

Сообщение Снег Север » 04.08.2020 11:09:18

Anika, а что вы хотите? В вашем примере действительно будет переполнение, чтобы его избежать нужен тип WholeData: QWord; При приведении к Cardinal в WholeData будет бессмысленный результат.
Аватара пользователя
Снег Север
долгожитель
 
Сообщения: 2637
Зарегистрирован: 27.11.2007 16:14:47

Re: Преобразование типов при вычислении

Сообщение Anika » 04.08.2020 11:30:50

Снег Север

Не поняла, почему здесь должно произойти переполнение?
WholeData: Cardinal ( = Longword): 0 .. 4294967295, т.е. от 0 до $FFFFFFFF
ReturnData0: Byte: 0..255, т.е. от 0 до $FF

При умножении даже самого большого значения типа byte $FF на $1000000, результат никогда не превысит максимальное значение типа Cardinal, т.к. $FF000000 явно меньше, чем $FFFFFFFF

В данном конкретном случае переполнение не должно происходить, т.к. я получаю 4 байта от контроллера, из них составляю 4 байтовое Wholeword.
Т.е. фактически ReturnData0 - это старший байт четырехбайтового значения WholeData. Я умножаю его на $1000000, чтобы поставить на нужное место в WholeData
Anika
незнакомец
 
Сообщения: 5
Зарегистрирован: 03.08.2020 15:46:06

Re: Преобразование типов при вычислении

Сообщение Снег Север » 04.08.2020 12:01:04

Хм... В вики пишут:
However, because of 32 and 64 bit little endian systems, using the Cardinal type is not recommended anymore for memory/pointer operations/arithmetic. It's recommended to use NativeInt or NativeUInt types instead. These types will match the width of the CPU registers which can be used to encode a memory address and so will always be the right size. For example under a 64b-bit OS, a NativeUInt will be like a UInt64 or a QuadWord and under a 32-bit OS, a NativeUInt will be like a DWord or a Cardinal.

Действительно, я в коде
Код: Выделить всё
var
   ReturnData0: byte;
   WholeData: Cardinal;
begin
  ReturnData0:=230;
  WholeData:=ReturnData0*$1000000;
  Edit1.Text := Format('%d',[WholeData]);
end; 

получаю ошибку в Format('%d',[WholeData]); Если заменить на Edit1.Text := IntToStr(WholeData); то ошибок нет.
Но если, как советует вики, сделать WholeData: NativeUInt; то ошибок не будет в обоих случаях.

Итого - лучше не использовать Cardinal, а использовать NativeUInt (или NativeInt).
Аватара пользователя
Снег Север
долгожитель
 
Сообщения: 2637
Зарегистрирован: 27.11.2007 16:14:47

Re: Преобразование типов при вычислении

Сообщение Anika » 04.08.2020 12:34:36

Снег Север

Спасибо, про Cardinal поняла, постараюсь избавиться от них в проекте.

Но замена на другой тип, к сожалению, не помогла.
Перепробовала все варианты: NativeUInt, UInt64, Int64 - все варианты приводят к ошибке.

Помогает только промежуточное присваивание временной переменной типа NativeUInt, UInt64, Int64, Cardinal.

Провела еще эксперимент:
Код: Выделить всё
var
WholeData: NativeUInt;
BByte: byte;

begin
   BByte:=127;
   WholeData:=BByte*$1000000;

   WholeData:=128*$1000000;

   BByte:=128;
   WholeData:=BByte*$1000000;    //ошибка появляется на этой строке
end;


Прихожу к выводу, что Lazarus при вычислении считает тип byte как -128 .. 127. Но при этом при отключении проверок отладчика "Диапазон (-Cr)" и "Вызов методов (-CR)" результат все равно правильный.
Т.е. Vadim был прав, указав, что изначальная ошибка в смешении типов при вычислении. Придется править весь код :(

На всякий случай - windows 7 64 bit, lazarus 2.0.6, FPC 3.0.4

Добавлено спустя 5 минут 17 секунд:
На всякий случай на картинке настройки отладчика.
У вас нет необходимых прав для просмотра вложений в этом сообщении.
Anika
незнакомец
 
Сообщения: 5
Зарегистрирован: 03.08.2020 15:46:06

Re: Преобразование типов при вычислении

Сообщение runewalsh » 04.08.2020 14:01:49

У меня падает (под $OVERFLOWCHECKS, разумеется) на 32-битном компиляторе и не падает на 64-битном. Так и задумано. Из документации:
3. The result of binary arithmetic operators (+, -, *, etc.) is determined in the following way:
  a. If at least one of the operands is larger than the native integer size, the result is chosen to be the smallest type that encompasses the ranges of the types of both operands. This means that mixing an unsigned with a smaller or equal in size signed will produce a signed type that is larger than both of them.
  b. If both operands have the same signedness, the result is the same type as them. The only exception is subtracting (-): in the case of unsigned - unsigned subtracting produces a signed result in FPC (as in Delphi, but not in TP7).
  c. Mixing signed and unsigned operands of the “native” int size produces a larger signed result. This means that mixing longint and longword on 32-bit platforms will produce an int64. Similarly, mixing byte and shortint on 8-bit platforms (AVR) will produce a smallint.

Пункт b утверждает, что результат операции, кроме вычитания, над типами одинаковой знаковости даёт результат той же знаковости.
-НО-
Угадайте, каким типом в выражении считается константа $1000000?
Не угадали: int32.

Не могу с ходу найти, но эти правила тоже где-то были. Вроде бы за тип целочисленного литерала берётся минимальный из тех, кто может его вместить, причём если константа помещается в знаковый — то ему отдаётся предпочтение перед беззнаковым того же размера.
Код: Выделить всё
writeln(Low(127), ' ~ ', High(127)); // -128 ~ 127
writeln(Low(128), ' ~ ', High(128)); // 0 ~ 255

То есть типом константы 127 считается int8, а 128 — uint8.

Поэтому константу, для которой критично (без)знаковое поведение, нужно явно привести к (без)знаковому типу:
Код: Выделить всё
WholeData := BByte * uint32($1000000);

(Вообще-то в Дельфи, тем более $MODE DELPHI, должны быть такие же правила, скорее всего, там просто была выключена проверка переполнения.)
Аватара пользователя
runewalsh
постоялец
 
Сообщения: 471
Зарегистрирован: 27.04.2010 00:15:25

Re: Преобразование типов при вычислении

Сообщение Vadim » 04.08.2020 14:17:00

Anika писал(а):Прихожу к выводу, что Lazarus при вычислении считает тип byte как -128 .. 127.

Это вряд ли... :-) Lazarus ведь работает на основе fpc, так что считать он будет то же, что и fpc (см. рисунок).
001.png


И ещё, на всякий случай (из документации к FreePascal):
Смешивание знаковых и беззнаковых операндов «родного» размера int приводит к бОльшему результату со знаком. Это означает, что смешивание longint и longword на 32-битных платформах приведет к образованию int64...

Поскольку у Вас там присутствует Cardinal, который компилятор автоматом переаодит в Longword, то в результате у Вас получится тип Int64. Переменная результата имеет такой тип? Вот Вам ещё один источник потенциальной ошибки. Как только Вы отключаете у компилятора проверку на переполнение, он Вам без зазрения совести подсунет неправильный результат, а Вы этого даже не заметите. ;-)
У вас нет необходимых прав для просмотра вложений в этом сообщении.
Vadim
долгожитель
 
Сообщения: 3918
Зарегистрирован: 05.10.2006 08:52:59
Откуда: Красноярск

Re: Преобразование типов при вычислении

Сообщение runewalsh » 04.08.2020 14:41:07

Vadim писал(а):Поскольку у Вас там присутствует Cardinal, который компилятор автоматом переаодит в Longword, то в результате у Вас получится тип Int64

Нет. Cardinal * Byte = Cardinal, оба типа беззнаковые, это ерунда (да и Cardinal везде эквивалентен LongWord, ты с чем-то путаешь). Ошибка происходит из-за byte * int32. Чтобы это посчитать, компилятор приводит оба операнда к NativeInt и перемножает их, ожидая снова получить NativeInt. И если NativeInt сам был int32, а результат в него не помещается (230 * $1000000 > High(int32)), то вычисление переполняется прежде, чем успевает преобразоваться к типу присваиваемой переменной (Cardinal).

Падать этому промежуточному переполнению на $OVERFLOWCHECKS абсолютно правильно, т. к. в общем случае оно испортит результат. Хотя изначальный код
Код: Выделить всё
var
   ReturnData0: byte;
   WholeData: Cardinal;
begin
   ReturnData0 := 230;
   WholeData := ReturnData0 * $1000000;
   writeln(WholeData);
end.

по случайности (из-за представления знаковых чисел в дополнительном коде и совпадения размеров NativeInt и Cardinal на x86-32) работает как надо, но вот

Код: Выделить всё
var
   ReturnData0: byte;
   WholeData: int64; // или uint64
begin
   ReturnData0 := 230;
   WholeData := ReturnData0 * $1000000;
   writeln(WholeData);
end.

— выдаёт неправильный результат под x86-32, как раз из-за переполнения NativeInt.

Исправленная версия везде работает как надо:
Код: Выделить всё
var
   ReturnData0: byte;
   WholeData: Cardinal;
begin
   ReturnData0 := 230;
   WholeData := ReturnData0 * Cardinal($1000000);
   writeln(WholeData);
end.

Потому что в ней типом промежуточных вычислений становится NativeUint.
Аватара пользователя
runewalsh
постоялец
 
Сообщения: 471
Зарегистрирован: 27.04.2010 00:15:25

Re: Преобразование типов при вычислении

Сообщение Vadim » 04.08.2020 15:27:34

runewalsh писал(а):Нет. Cardinal * Byte = Cardinal, оба типа беззнаковые, это ерунда (да и Cardinal везде эквивалентен LongWord, ты с чем-то путаешь).

Я то как раз ни с чем не путаю, поскольку написал:
Vadim писал(а):на всякий случай

;-)
Vadim
долгожитель
 
Сообщения: 3918
Зарегистрирован: 05.10.2006 08:52:59
Откуда: Красноярск

Re: Преобразование типов при вычислении

Сообщение Anika » 04.08.2020 15:38:33

runewalsh

Код: Выделить всё
Поэтому константу, для которой критично (без)знаковое поведение, нужно явно привести к (без)знаковому типу:


Вот!!! Вот оно!!! Я даже и думать забыла, что константы тоже должны быть какого-то типа.

(Вообще-то в Дельфи, тем более $MODE DELPHI, должны быть такие же правила, скорее всего, там просто была выключена проверка переполнения.)

Очень даже вероятно. Скорее всего, так и было, но сейчас уже никак не проверить.
Anika
незнакомец
 
Сообщения: 5
Зарегистрирован: 03.08.2020 15:46:06

Re: Преобразование типов при вычислении

Сообщение Сквозняк » 04.08.2020 17:41:29

runewalsh писал(а):Код: Выделить всё
Код: Выделить всё
    var
       ReturnData0: byte;
       WholeData: int64; // или uint64
    begin
       ReturnData0 := 230;
       WholeData := ReturnData0 * $1000000;
       writeln(WholeData);
    end.


— выдаёт неправильный результат под x86-32, как раз из-за переполнения NativeInt.


Можно ли директивой компиляции заставить компилятор собирать этот код правильно, без переполнений?
Сквозняк
энтузиаст
 
Сообщения: 776
Зарегистрирован: 29.06.2006 22:08:32

Re: Преобразование типов при вычислении

Сообщение runewalsh » 04.08.2020 18:36:49

Сквозняк писал(а):Можно ли директивой компиляции заставить компилятор собирать этот код под x86-32 как под x86-64?

Нет, только руками привести один из операндов к нужному типу: константу к Cardinal, или любой операнд к int64 (второй способ более общий, но медленный).

Я понимаю твои чувства, но если бы компилятор делал это автоматически, это бы замедлило все вычисления в x86-32 минимум в два раза почём зря. Абсолютное большинство выражений никогда не словят такое переполнение, так что расширение до int64 им не нужно, а замедление от выхода за рамки нативного типа останется. Подсказка
Hint: Mixing signed expressions and longwords gives a 64bit result

специально для подсвечивания таких случаев и сделана.

А сдвоенный тип int128 (аналог в x86-64 того, чем для x86-32 является int64) и вовсе, насколько я понимаю, не реализован, так что код вида
Код: Выделить всё
var
   a: int64;
begin
   a := High(int64);
   writeln((a + 1) div 2);
end;

по аналогии выдаёт неправильный результат на x64, и на этот раз обходного пути, каким был бы int128, нет.

А даже если бы у нас был int128, для него были бы аналогичные крайние случаи с переполнением, для обхода которых понадобился бы уже int256. Где же остановиться? Теперь вариант не лезть в дебри и по умолчанию использовать нативный размер кажется не таким уж и плохим.
Аватара пользователя
runewalsh
постоялец
 
Сообщения: 471
Зарегистрирован: 27.04.2010 00:15:25

След.

Вернуться в Free Pascal Compiler

Кто сейчас на конференции

Сейчас этот форум просматривают: ev и гости: 5

Рейтинг@Mail.ru