Разница в приведении типа float к int, 32-битный C
В настоящее время я работаю со старым кодом, который должен работать на 32-битной системе. Во время этой работы я наткнулся на проблему, из-за которой (из академического интереса) я хотел бы понять причину.
Кажется, что приведение от float к 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-generic (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
Любые указатели, которые помогут мне понять, что здесь происходит, приветствуются.
Ответы
Ответ 1
С 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
результат затем преобразуется обратно в число с плавающей точкой и сохраняется, а затем снова извлекается и помещается в стек FPU (и снова преобразуется в удвоение) для вызова __ftol2_sse
, функции времени выполнения для преобразования double в int.
Для c2
промежуточное значение не преобразуется в и из числа с плавающей точкой и сразу __ftol2_sse
функцию __ftol2_sse
. Для этой функции см. Также ответ в Convert double to int? ,
Ассемблер:
f10 = f1*10;
fld dword ptr [f1]
fmul qword ptr [[email protected] (496190h)]
fstp dword ptr [f10]
c2 = (int)(f1*10);
fld dword ptr [f1]
fmul qword ptr [[email protected] (496190h)]
call __ftol2_sse
mov dword ptr [c2],eax
c1 = (int)f10;
fld dword ptr [f10]
call __ftol2_sse
mov dword ptr [c1],eax
Ответ 2
В "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.1
, 0.1
является double
константой, поэтому арифметика выполняется в double
, и результат составляет 3,100000000000000088817841970012523233890533447265625. Затем, чтобы присвоить его f1
, он преобразуется в число с float
, что дает 3.099999904632568359375. - В
f10 = f1*10.0;
, 10.0
- double
константа, поэтому арифметика снова выполняется в double
, и результат равен 30,999999904632568359375. Для присвоения f10
он преобразуется в число с float
, и в результате получается 31. - Позже, когда печатаются
f10
и f1*10.0
, мы видим значения, приведенные выше, с девятью цифрами после десятичной точки, "31.000000000" для f10
и "30.999999046".
Если вы напечатаете f1*10.0f
, с константой с float
10.0f
вместо double
константы 10.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
, сомнительно, что наблюдения были представлены точно. Чтобы прояснить это, должен быть показан точный код, а наблюдения должны быть воспроизведены третьей стороной.
Ответ 3
Стандарт C не очень строг в отношении математики с плавающей запятой. Стандарт позволяет реализации выполнять вычисления с более высокой точностью, чем используемые типы.
Результат в вашем случае может быть связан с тем, что c1
вычисляется как "float-to-int", а c2
вычисляется как "double-to-int" (или даже с более высокой точностью).
Вот еще один пример, демонстрирующий такое же поведение.
#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", и преобразование в int, следовательно, даст тот же результат.
Ответ 4
Основная причина заключается в том, что the rounding-control (RC) field of the x87 FPU control register
значений the rounding-control (RC) field of the x87 FPU control register
несовместимо в следующих двух строках. в конечном итоге значения с1 и с2 различны.
0x08048457 <+58>: fstps 0x44(%esp)
0x0804848b <+110>: fistpl 0x3c(%esp)
Добавьте опцию компиляции gcc -mfpmath=387 -mno-sse
, ее можно воспроизвести (даже без -m32 или изменить число с плавающей точкой на double)
Как это:
gcc -otest test.c -g -mfpmath=387 -mno-sse -m32
Затем используйте gdb для отладки, точку останова на 0x0804845b и запустите до я = 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 и запустите до я = 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 <[email protected]>
0x080484d9 <+188>: addl $0x1,0x4c(%esp)
0x080484de <+193>: cmpl $0x14,0x4c(%esp)
0x080484e3 <+198>: jle 0x8048433 <main+22>
0x080484e9 <+204>: leave
0x080484ea <+205>: ret