Разница в приведении float к int, 32-битный C

В настоящее время я работаю со старым кодом, который должен запускать 32-битную систему. В ходе этой работы я наткнулся на проблему, причину которой (из академического интереса) хотел бы понять.

Похоже, что приведение типов с плавающей запятой к типу int в 32-разрядном C ведет себя по-разному, если приведение выполняется к переменной или выражению. Рассмотрим программу:

#include <stdio.h>
int main() {
   int i,c1,c2;
   float f1,f10;
   for (i=0; i< 21; i++)  {
      f1 = 3+i*0.1;
      f10 = f1*10.0;
      c1 = (int)f10;
      c2 = (int)(f1*10.0);
      printf("%d, %d, %d, %11.9f, %11.9f\n",c1,c2,c1-c2,f10,f1*10.0);
   }
}

Скомпилированная (с использованием gcc) либо непосредственно в 32-битной системе, либо в 64-битной системе с использованием модификатора -m32 вывод программы:

30, 30, 0, 30.000000000 30.000000000
31, 30, 1, 31.000000000 30.999999046
32, 32, 0, 32.000000000 32.000000477
33, 32, 1, 33.000000000 32.999999523
34, 34, 0, 34.000000000 34.000000954
35, 35, 0, 35.000000000 35.000000000
36, 35, 1, 36.000000000 35.999999046
37, 37, 0, 37.000000000 37.000000477
38, 37, 1, 38.000000000 37.999999523
39, 39, 0, 39.000000000 39.000000954
40, 40, 0, 40.000000000 40.000000000
41, 40, 1, 41.000000000 40.999999046
42, 41, 1, 42.000000000 41.999998093
43, 43, 0, 43.000000000 43.000001907
44, 44, 0, 44.000000000 44.000000954
45, 45, 0, 45.000000000 45.000000000
46, 45, 1, 46.000000000 45.999999046
47, 46, 1, 47.000000000 46.999998093
48, 48, 0, 48.000000000 48.000001907
49, 49, 0, 49.000000000 49.000000954
50, 50, 0, 50.000000000 50.000000000 

Следовательно, ясно, что существует разница между приведением переменной и выражением. Обратите внимание, что проблема существует также, если float изменено на double и/или int изменено на short или long, а также проблема не проявляется, если программа скомпилирована как 64-битная.

Чтобы уточнить, проблема, которую я пытаюсь здесь понять, связана не с арифметикой/округлением с плавающей запятой, а скорее с различиями в обработке памяти в 32-разрядных версиях.

Проблема была проверена на:

  • Версия Linux 4.15.0-45-универсальная (buildd@lgw01-amd64-031) (версия gcc 7.3.0 (Ubuntu 7.3.0-16ubuntu3)), программа скомпилирована с использованием: gcc -m32 Cast32int.c

  • Linux версии 2.4.20-8 ([email protected]) (gcc версии 3.2.2 20030222 (Red Hat Linux 3.2.2-5)), программа скомпилирована с использованием: gcc Cast32int.c

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

добро пожаловать в мир математики с плавающей запятой - вы можете прочитать stackoverflow.com/questions/588004/…

4386427 26.02.2019 09:55

Не удается воспроизвести, какая у вас платформа/компилятор/версия и т. д.?

Jabberwocky 26.02.2019 09:55

Вставьте этот printf("%15.15f %15.15f\n", f1, f10); в цикл и покажите результат.

Jabberwocky 26.02.2019 09:59

Также не удается воспроизвести (windows/intel gcc 7.1.0 или Rasbian linux/ARM gcc 6.3.0)

Euan Smith 26.02.2019 10:02

MSVC не может воспроизвести, но я получаю одно предупреждение о потере данных при преобразовании double в float.

Weather Vane 26.02.2019 10:20

@WeatherVane это предупреждение здесь: f1 = 3 + i * 0.1;. Если поставить f после 0.1, это исчезнет, ​​но я сомневаюсь, что это изменит поведение на платформе OP.

Jabberwocky 26.02.2019 10:28

@Jabberwocky спасибо, я знал об этом.

Weather Vane 26.02.2019 10:34

@Jabberwocky разница в том, что в одном случае продукт помещается в переменную float, а в другом случае продукт переходит непосредственно в переменную int. Превращается ли floats в doubleдля умножения на int? Если бы умножение было на 10.0 (a double) вместо 10, это могло бы быть, но не обязательно, если результат будет таким же.

Weather Vane 26.02.2019 10:38

Пожалуйста, прочитайте 3-й комментарий и действуйте соответственно. Это может помочь узнать, что происходит.

Jabberwocky 26.02.2019 10:41

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

user694733 26.02.2019 10:52

Сообщение @Jabberwocky обновлено

cpaitor 26.02.2019 11:02

@user694733 полные команды компиляции были добавлены в сообщение

cpaitor 26.02.2019 11:02

Обновленный код умножается на 10.0, который является double. Возможно, float повышается до double перед умножением, а затем обратно до float перед преобразованием в int. В другом случае продукт double преобразуется непосредственно в int.

Weather Vane 26.02.2019 11:05

@Wheather Vane, не имеет значения, умножается ли число на 10 или 10,0, обновление до 10,0 было просто для проверки этого

cpaitor 26.02.2019 11:10

Мой комментарий подкрепляется ответом @PaulOgilvie.

Weather Vane 26.02.2019 11:12

На некоторых платформах и версиях GCC сопроцессор с плавающей запятой используется для промежуточных вычислений (то, что вы назвали «выражениями») при нацеливании на 32-разрядную архитектуру x86. Эти вычисления являются 80-битными. Однако в 64-битных системах это никогда не используется, и вместо этого вы получаете 64-битные вычисления SSE.

Nikos C. 26.02.2019 11:13

У Godbolt.org нет версии GCC еще 3.2.2, поэтому я не могу изучить это, но похоже, что код, сгенерированный 4.1.2 будет вести себя так же, как показано в вопросе. Обратите внимание, что различия между f1*10.0 и f10 полностью возникают из-за преобразования в float во время присвоения f10, и это преобразование предписано стандартом C; компилятор не может игнорировать его лицензией на использование большей точности в выражениях. Следовательно, если GCC 3.2.2 вел себя иначе, либо использовался флаг, отклоняющийся от стандарта C, либо это ошибка.

Eric Postpischil 26.02.2019 15:04

Возможный дубликат Разница в арифметике с плавающей запятой между x86 и x64

phuclv 26.02.2019 15:40

@phuclv Не уверен, как вы считаете эти сообщения дубликатами, поскольку они учитывают разницу между 32-битной и 64-битной версиями. Разница здесь для 32 бит

cpaitor 26.02.2019 18:11

почему нет? в основном речь идет о выражении, которое печатает разные значения в 32- и 64-битных сборках, как и другие вопросы с тем же объяснением, которое является более высокой оцениваемой точностью в x87. Вместо этого мыслите горизонтально по всем целям, и все они сводятся к одному и тому же ответу.

phuclv 27.02.2019 03:10
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
11
22
1 521
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Ответ принят как подходящий

С MS Visual C 2008 я смог воспроизвести это.

Проверяя ассемблер, разница между ними заключается в промежуточном сохранении и выборке результата с промежуточными преобразованиями:

  f10 = f1*10.0;          // double result f10 converted to float and stored
  c1 = (int)f10;          // float result f10 fetched and converted to double
  c2 = (int)(f1*10.0);    // no store/fetch/convert

Сгенерированный ассемблером помещает значения в стек FPU, которые преобразуются в 64-битные, а затем перемножаются. Для c1 результат затем преобразуется обратно в float и сохраняется, а затем снова извлекается и помещается в стек FPU (и снова преобразуется в double) для вызова __ftol2_sse, функции времени выполнения для преобразования double в int.

Для c2 промежуточное значение нет преобразуется в число с плавающей запятой и из него и немедленно передается в функцию __ftol2_sse. Для этой функции см. также ответ на Преобразовать двойное число в целое число?.

Ассемблер:

      f10 = f1*10;
fld         dword ptr [f1] 
fmul        qword ptr [__real@4024000000000000 (496190h)] 
fstp        dword ptr [f10] 

      c2 = (int)(f1*10);
fld         dword ptr [f1] 
fmul        qword ptr [__real@4024000000000000 (496190h)] 
call        __ftol2_sse
mov         dword ptr [c2],eax 

      c1 = (int)f10;
fld         dword ptr [f10] 
call        __ftol2_sse
mov         dword ptr [c1],eax 

Магазины и выборки несущественны; они просто реализации семантики C. Основная причина различий между f1*10.0 и f10 заключается в том, что первое является выражением double, а второе — float. Присваивание f10 = f1*10.0; изменяет значение при преобразовании double в float.

Eric Postpischil 26.02.2019 14:07

@EricPostpischil Ты уверен? Я почти уверен, что f1*10.0 будет иметь точность long double (80 бит), а не двойную (всего 64 бита).

Martin Bonner supports Monica 26.02.2019 14:15

@MartinBonner: это не имеет значения. f10 — поплавок; на этой платформе его значение составляет 24 бита. Число 10 имеет три значащих бита, поэтому умножение на него дает число, состоящее не более чем из 27 бит. Таким образом, его точное значение будет соответствовать 53-битной мантиссе double. Расширение до 64 бит ничего не изменит.

Eric Postpischil 26.02.2019 14:46

Я думаю, что правильный ответ - это действительно наблюдение @EricPostpischil о том, что в C все вычисления с плавающей запятой выполняются с двойной точностью. Следовательно, исходный код ведет себя так, как задумано. Мой ответ — просто идентификация того, что происходит «под капотом».

Paul Ogilvie 26.02.2019 14:50

@PaulOgilvie: Дело не в том, что все вычисления с плавающей запятой выполняются в double, а в том, что использование одного операнда типа double приводит к преобразованию других операндов в double в соответствии с обычные арифметические преобразования. Если все операнды имеют тип float, как при использовании 10.0f вместо 10.0, тип выражения остается float (хотя реализации разрешено использовать double для его оценки, но, похоже, этого не происходит в коде OP).

Eric Postpischil 26.02.2019 14:57

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

Результат в вашем случае, вероятно, будет связан с тем фактом, что c1 рассчитывается как «с плавающей запятой в целое», а c2 рассчитывается как «двойное с целым» (или даже с более высокой точностью).

Вот еще один пример, показывающий такое же поведение.

#define DD 0.11111111

int main()
{
  int i = 27;

  int c1,c2,c3;
  float f1;
  double d1;
  printf("%.60f\n", DD);

  f1 = i * DD;
  d1 = i * DD;
  c1 = (int)f1;
  c2 = (int)(i * DD);
  c3 = (int)d1;

  printf("----------------------\n");
  printf("f1: %.60f\n", f1);
  printf("d1: %.60f\n", d1);
  printf("m : %.60f\n", i * DD);
  printf("%d, %d, %d\n",c1,c2,c3);
}

Мой вывод:

0.111111109999999999042863407794357044622302055358886718750000
----------------------
f1: 3.000000000000000000000000000000000000000000000000000000000000
d1: 2.999999970000000182324129127664491534233093261718750000000000
m : 2.999999970000000182324129127664491534233093261718750000000000
3, 2, 2

Хитрость здесь заключается в количестве единиц в 0.11111111. Точный результат: «2,99999997». Когда вы меняете количество единиц, точный результат остается в форме «2,99...997» (т. е. число 9 увеличивается при увеличении числа 1).

В какой-то момент (то есть некоторое количество единиц) вы достигнете точки, в которой сохранение результата в число с плавающей запятой округляет результат до «3,0», в то время как двойное число все еще может удерживать «2,999999 .....». Тогда преобразование в int даст другие результаты.

Дальнейшее увеличение числа единиц приведет к тому, что значение double также будет округлено до «3,0», и преобразование в целое, следовательно, даст тот же результат.

На некоторых платформах и версиях GCC сопроцессор с плавающей запятой используется для промежуточных вычислений (то, что ОП назвал «выражениями»). Это 80-битные. Однако в 64-битных системах это никогда не используется, и вместо этого вы получаете 64-битные вычисления SSE.

Nikos C. 26.02.2019 11:12

Различия не возникают в преобразованиях в int. Разница возникает, когда f1*10.0, которое является выражением double, присваивается f10, которое является float. Это присвоение изменяет значение.

Eric Postpischil 26.02.2019 14:05

@EricPostpischil Именно это я и пытался написать. Здесь: "... достичь точки, в которой сохранение результата в число с плавающей запятой округляет результат до "3.0""

4386427 26.02.2019 15:43

В «32-битной системе» разница вызвана тем, что f1*10.0 использует полную double точность, а f10 имеет только float точность, потому что это ее тип. f1*10.0 использует double точность, потому что 10.0 является double константой. Когда f1*10.0 присваивается f10, значение изменяется, потому что оно неявно преобразуется в float, что имеет меньшую точность.

Если вместо этого вы используете float константу 10.0f, различия исчезают.

Рассмотрим первый случай, когда i равно 1. Тогда:

  • В f1 = 3+i*0.10.1 является константой double, поэтому арифметика выполняется в double, и результат равен 3,100000000000000088817841970012523233890533447265625. Затем, чтобы присвоить это f1, оно преобразуется в float, что дает 3,099999904632568359375.
  • В f10 = f1*10.0;10.0 является константой double, поэтому арифметика снова выполняется в double, и результат равен 30,99999904632568359375. Для присвоения f10 это преобразуется в float, и результат равен 31.
  • Позже, когда f10 и f1*10.0 печатаются, мы видим приведенные выше значения с девятью цифрами после запятой, «31.000000000» для f10 и «30.999999046».

Если вы напечатаете f1*10.0f с константой float10.0f вместо константы double10.0, результатом будет «31.000000000», а не «30.999999046».

(В приведенном выше примере используется базовая 32-битная и 64-битная двоичная арифметика с плавающей запятой стандарта IEEE-754.)

В частности, обратите внимание: разница между f1*10.0 и f10 возникает, когда f1*10.0 преобразуется в float для присвоения f10. Хотя C позволяет реализациям использовать дополнительную точность при вычислении выражений, он требует, чтобы реализации отбрасывали эту точность при присваиваниях и приведениях. Следовательно, в компиляторе, соответствующем стандарту, присваивание f10должен использует точность float. Это означает, что даже когда программа скомпилирована для «64-битной системы», возникают различия должен. Если они этого не делают, компилятор не соответствует стандарту C.

Кроме того, если float изменить на double, преобразования в float не произойдет, и значение не изменится. В этом случае не должно проявляться никаких различий между f1*10.0 и f10.

Учитывая, что в вопросе сообщается, что различия не проявляются при «64-битной» компиляции и проявляются при double, сомнительно, что наблюдения были сообщены точно. Для уточнения этого следует показать точный код, а наблюдения воспроизвести третьей стороной.

Вопрос заключался в том, почему существует разница между сборкой -m32 и -m64 исходного кода такой же, а не в чем разница между 10.0 и 10.0f.

Nikos C. 26.02.2019 14:48

@NikosC.: Действительно, но в вопросе ложно говорится: «очевидно, что существует разница между приведением переменной и выражением». Этот ответ опровергает это и объясняет разницу. Вопрос не дает убедительных доказательств того, что между 32-битной и 64-битной компиляциями есть разница - я подозреваю, что это ошибка пользователя. Я обновил ответ, чтобы отметить проблему.

Eric Postpischil 26.02.2019 14:53

Основная причина в том, что значения the rounding-control (RC) field of the x87 FPU control register несовместимы в следующих двух строках. в конечном итоге значения c1 и c2 различны.

0x08048457 <+58>:    fstps  0x44(%esp)
0x0804848b <+110>:   fistpl 0x3c(%esp)

Добавьте параметр компиляции gcc -mfpmath=387 -mno-sse, его можно воспроизвести (даже без -m32 или изменить float на double)
Нравится:

gcc -otest test.c -g -mfpmath=387 -mno-sse -m32

Затем используйте gdb для отладки, установите точку останова на 0x0804845b и запустите i=1.

    0x08048457 <+58>:    fstps  0x44(%esp)
    0x0804845b <+62>:    flds   0x44(%esp)

    (gdb) info float
    =>R7: Valid   0x4003f7ffff8000000000 +30.99999904632568359      
      R6: Empty   0x4002a000000000000000
      R5: Empty   0x00000000000000000000
      R4: Empty   0x00000000000000000000
      R3: Empty   0x00000000000000000000
      R2: Empty   0x00000000000000000000
      R1: Empty   0x00000000000000000000
      R0: Empty   0x00000000000000000000

    Status Word:         0x3820                  PE                        
                           TOP: 7
    Control Word:        0x037f   IM DM ZM OM UM PM
                           PC: Extended Precision (64-bits)
                           RC: Round to nearest
    Tag Word:            0x3fff
    Instruction Pointer: 0x00:0x08048455
    Operand Pointer:     0x00:0x00000000
    Opcode:              0x0000

    (gdb) x /xw 0x44+$esp
    0xffffb594:     0x41f80000 ==> 31.0, s=0, M=1.1111 E=4

наблюдать за результатами выполнения fstps,
в это время значение RC в управляющем регистре fpu равно Округлить до ближайшего.
значение в регистре fpu — 30,99999904632568359 (80 бит).
значение 0x44(%esp) (variable "f10") равно 31,0. (округлить до ближайшего)

Затем используйте gdb для отладки, установите точку останова на 0x0804848b и запустите i=1.

    0x0804848b <+110>:   fistpl 0x3c(%esp)

    (gdb) info float
    =>R7: Valid   0x4003f7ffff8000000000 +30.99999904632568359      
      R6: Empty   0x4002a000000000000000
      R5: Empty   0x00000000000000000000
      R4: Empty   0x00000000000000000000
      R3: Empty   0x00000000000000000000
      R2: Empty   0x00000000000000000000
      R1: Empty   0x00000000000000000000
      R0: Empty   0x00000000000000000000

    Status Word:         0x3820                  PE                        
                           TOP: 7
    Control Word:        0x0c7f   IM DM ZM OM UM PM
                           PC: Single Precision (24-bits)
                           RC: Round toward zero
    Tag Word:            0x3fff
    Instruction Pointer: 0x00:0x08048485
    Operand Pointer:     0x00:0x00000000
    Opcode:              0x0000

в это время значение RC в регистре управления на fpu равно Округлить до нуля.
значение в регистре fpu — 30,99999904632568359 (80 бит). значение такое же, как указано выше
очевидно, что при преобразовании целого числа десятичная точка усекается, и значение равно 30.

Ниже приведен main декомпилированный код

    (gdb) disas main
    Dump of assembler code for function main:
       0x0804841d <+0>:     push   %ebp
       0x0804841e <+1>:     mov    %esp,%ebp
       0x08048420 <+3>:     and    $0xfffffff0,%esp
       0x08048423 <+6>:     sub    $0x50,%esp
       0x08048426 <+9>:     movl   $0x0,0x4c(%esp)
       0x0804842e <+17>:    jmp    0x80484de <main+193>
       0x08048433 <+22>:    fildl  0x4c(%esp)
       0x08048437 <+26>:    fldl   0x80485a8
       0x0804843d <+32>:    fmulp  %st,%st(1)
       0x0804843f <+34>:    fldl   0x80485b0
       0x08048445 <+40>:    faddp  %st,%st(1)
       0x08048447 <+42>:    fstps  0x48(%esp)
       0x0804844b <+46>:    flds   0x48(%esp)
       0x0804844f <+50>:    flds   0x80485b8
       0x08048455 <+56>:    fmulp  %st,%st(1)
       0x08048457 <+58>:    fstps  0x44(%esp)        // store to f10
       0x0804845b <+62>:    flds   0x44(%esp)
       0x0804845f <+66>:    fnstcw 0x2a(%esp)
       0x08048463 <+70>:    movzwl 0x2a(%esp),%eax
       0x08048468 <+75>:    mov    $0xc,%ah
       0x0804846a <+77>:    mov    %ax,0x28(%esp)
       0x0804846f <+82>:    fldcw  0x28(%esp)
       0x08048473 <+86>:    fistpl 0x40(%esp)
       0x08048477 <+90>:    fldcw  0x2a(%esp)
       0x0804847b <+94>:    flds   0x48(%esp)
       0x0804847f <+98>:    fldl   0x80485c0
       0x08048485 <+104>:   fmulp  %st,%st(1)
       0x08048487 <+106>:   fldcw  0x28(%esp)
       0x0804848b <+110>:   fistpl 0x3c(%esp)       // f1 * 10 convert int
       0x0804848f <+114>:   fldcw  0x2a(%esp)
       0x08048493 <+118>:   flds   0x48(%esp)
       0x08048497 <+122>:   fldl   0x80485c0
       0x0804849d <+128>:   fmulp  %st,%st(1)
       0x0804849f <+130>:   flds   0x44(%esp)
       0x080484a3 <+134>:   fxch   %st(1)
       0x080484a5 <+136>:   mov    0x3c(%esp),%eax
       0x080484a9 <+140>:   mov    0x40(%esp),%edx
       0x080484ad <+144>:   sub    %eax,%edx
       0x080484af <+146>:   mov    %edx,%eax
       0x080484b1 <+148>:   fstpl  0x18(%esp)
       0x080484b5 <+152>:   fstpl  0x10(%esp)
       0x080484b9 <+156>:   mov    %eax,0xc(%esp)
       0x080484bd <+160>:   mov    0x3c(%esp),%eax
       0x080484c1 <+164>:   mov    %eax,0x8(%esp)
       0x080484c5 <+168>:   mov    0x40(%esp),%eax
       0x080484c9 <+172>:   mov    %eax,0x4(%esp)
       0x080484cd <+176>:   movl   $0x8048588,(%esp)
       0x080484d4 <+183>:   call   0x80482f0 <printf@plt>
       0x080484d9 <+188>:   addl   $0x1,0x4c(%esp)
       0x080484de <+193>:   cmpl   $0x14,0x4c(%esp)
       0x080484e3 <+198>:   jle    0x8048433 <main+22>
       0x080484e9 <+204>:   leave  
       0x080484ea <+205>:   ret

Другие вопросы по теме