Câu trả lời của Pascal là OK - nhưng thiếu chi tiết dẫn đến một số người dùng không hiểu ;-). Nếu bạn quan tâm đến cách nó trông như thế nào ở cấp độ thấp hơn (giả sử bộ đồng xử lý chứ không phải phần mềm xử lý các hoạt động dấu phẩy động) - hãy đọc tiếp.
Trong 32 bit float (IEEE 754), bạn có thể lưu trữ tất cả các số nguyên trong phạm vi [-2 24 ... 2 24 ] . Các số nguyên bên ngoài phạm vi cũng có thể có biểu diễn chính xác dưới dạng float nhưng không phải tất cả chúng đều có. Vấn đề là bạn chỉ có thể có 24 bit quan trọng để chơi với float.
Đây là cách chuyển đổi từ int-> float thường trông như thế nào ở cấp độ thấp:
fild dword ptr[your int]
fstp dword ptr[your float]
Nó chỉ là chuỗi 2 lệnh của bộ đồng xử lý. Đầu tiên tải int 32 bit vào ngăn xếp của bộ xử lý và chuyển nó thành float rộng 80 bit.
Sách hướng dẫn dành cho nhà phát triển phần mềm kiến trúc Intel® 64 và IA-32
(LẬP TRÌNH VỚI FPU X87):
Khi các giá trị số nguyên dấu phẩy động, số nguyên hoặc số nguyên BCD được đóng gói được tải từ bộ nhớ vào bất kỳ thanh ghi dữ liệu FPU nào trong số các thanh ghi dữ liệu x87 FPU, các giá trị sẽ tự động được chuyển đổi thành định dạng dấu phẩy động có độ chính xác kép mở rộng (nếu chúng chưa có ở định dạng đó).
Vì các thanh ghi FPU là float rộng 80bit - không có vấn đề gì fild
ở đây vì int 32bit hoàn toàn phù hợp với ý nghĩa 64bit và định dạng dấu phẩy động.
Càng xa càng tốt.
Phần thứ hai - fstp
hơi phức tạp và có thể gây ngạc nhiên. Nó được cho là lưu trữ dấu chấm động 80bit trong float 32bit. Mặc dù nó là tất cả về các giá trị số nguyên (trong câu hỏi) bộ đồng xử lý có thể thực sự thực hiện 'làm tròn'. Ke? Làm thế nào để bạn làm tròn giá trị số nguyên ngay cả khi nó được lưu trữ ở định dạng dấu phẩy động? ;-).
Tôi sẽ giải thích ngay - trước tiên hãy xem x87 cung cấp những chế độ làm tròn nào (chúng là hiện thân của các chế độ làm tròn IEE 754). X87 fpu có 4 chế độ làm tròn được điều khiển bởi các bit # 10 và # 11 của từ điều khiển của fpu:
- 00 - đến chẵn gần nhất - Kết quả làm tròn là kết quả gần nhất với kết quả chính xác vô hạn. Nếu hai giá trị gần bằng nhau, kết quả là giá trị chẵn (nghĩa là giá trị có bit nhỏ nhất bằng 0). Mặc định
- 01 - về phía -Inf
- 10 - hướng + inf
- 11 - về phía 0 (tức là. Cắt ngắn)
Bạn có thể chơi với các chế độ làm tròn bằng cách sử dụng mã đơn giản này (mặc dù nó có thể được thực hiện theo cách khác - hiển thị mức thấp ở đây):
enum ROUNDING_MODE
{
RM_TO_NEAREST = 0x00,
RM_TOWARD_MINF = 0x01,
RM_TOWARD_PINF = 0x02,
RM_TOWARD_ZERO = 0x03 // TRUNCATE
};
void set_round_mode(enum ROUNDING_MODE rm)
{
short csw;
short tmp = rm;
_asm
{
push ax
fstcw [csw]
mov ax, [csw]
and ax, ~(3<<10)
shl [tmp], 10
or ax, tmp
mov [csw], ax
fldcw [csw]
pop ax
}
}
Ok tốt nhưng vẫn còn đó là liên quan đến các giá trị số nguyên như thế nào? Kiên nhẫn ... để hiểu tại sao bạn có thể cần các chế độ làm tròn liên quan đến chuyển đổi int sang float, hãy kiểm tra cách chuyển đổi int thành float rõ ràng nhất - truncation (không phải mặc định) - có thể trông như thế này:
- ký lục
- phủ định số nguyên của bạn nếu nhỏ hơn 0
- tìm vị trí ngoài cùng bên trái 1
- shift int sang phải / trái để 1 được tìm thấy ở trên được định vị trên bit # 23
- ghi lại số ca trong quá trình để bạn có thể tính toán số mũ
Và mã mô phỏng bahavior này có thể trông như thế này:
float int2float(int value)
{
// handles all values from [-2^24...2^24]
// outside this range only some integers may be represented exactly
// this method will use truncation 'rounding mode' during conversion
// we can safely reinterpret it as 0.0
if (value == 0) return 0.0;
if (value == (1U<<31)) // ie -2^31
{
// -(-2^31) = -2^31 so we'll not be able to handle it below - use const
value = 0xCF000000;
return *((float*)&value);
}
int sign = 0;
// handle negative values
if (value < 0)
{
sign = 1U << 31;
value = -value;
}
// although right shift of signed is undefined - all compilers (that I know) do
// arithmetic shift (copies sign into MSB) is what I prefer here
// hence using unsigned abs_value_copy for shift
unsigned int abs_value_copy = value;
// find leading one
int bit_num = 31;
int shift_count = 0;
for(; bit_num > 0; bit_num--)
{
if (abs_value_copy & (1U<<bit_num))
{
if (bit_num >= 23)
{
// need to shift right
shift_count = bit_num - 23;
abs_value_copy >>= shift_count;
}
else
{
// need to shift left
shift_count = 23 - bit_num;
abs_value_copy <<= shift_count;
}
break;
}
}
// exponent is biased by 127
int exp = bit_num + 127;
// clear leading 1 (bit #23) (it will implicitly be there but not stored)
int coeff = abs_value_copy & ~(1<<23);
// move exp to the right place
exp <<= 23;
int ret = sign | exp | coeff;
return *((float*)&ret);
}
Bây giờ ví dụ - chế độ cắt ngắn chuyển đổi 2147483583
thành 2147483520
.
2147483583 = 01111111_11111111_11111111_10111111
Trong quá trình chuyển đổi int-> float, bạn phải chuyển 1 ngoài cùng bên trái sang bit # 23. Bây giờ hàng đầu 1 ở bit # 30. Để đặt nó vào bit # 23, bạn phải thực hiện dịch sang phải 7 vị trí. Trong thời gian đó, bạn bị lỏng (chúng sẽ không phù hợp với định dạng float 32 bit) 7 bit lsb từ bên phải (bạn cắt / cắt). Họ đã:
01111111 = 63
Và 63 là số ban đầu bị mất:
2147483583 -> 2147483520 + 63
Việc cắt bớt là dễ dàng nhưng có thể không nhất thiết là điều bạn muốn và / hoặc là tốt nhất cho mọi trường hợp. Hãy xem xét ví dụ dưới đây:
67108871 = 00000100_00000000_00000000_00000111
Giá trị trên không thể được đại diện chính xác bằng float nhưng hãy kiểm tra xem việc cắt ngắn có tác dụng gì với nó. Như trước đây - chúng ta cần chuyển 1 ngoài cùng bên trái sang bit # 23. Điều này yêu cầu giá trị phải được dịch sang phải chính xác 3 vị trí làm mất 3 bit LSB (kể từ bây giờ tôi sẽ viết các số khác nhau để hiển thị bit thứ 24 ngầm định của float là ở đâu và sẽ đóng ngoặc rõ ràng 23 bit có ý nghĩa và):
00000001.[0000000_00000000_00000000] 111 * 2^26 (3 bits shifted out)
Việc cắt ngắn cắt 3 bit ở cuối để lại cho chúng ta 67108864
(67108864 + 7 (3 bit được cắt nhỏ)) = 67108871 (hãy nhớ rằng mặc dù chúng ta dịch chuyển chúng ta bù trừ bằng thao tác số mũ - bị bỏ qua ở đây).
Như vậy đã đủ tốt chưa? Hey 67108872
hoàn toàn có thể biểu diễn bằng float 32bit và sẽ tốt hơn nhiều 67108864
phải không? ĐÚNG và đây là nơi bạn có thể muốn nói về việc làm tròn khi chuyển đổi int thành 32bit float.
Bây giờ chúng ta hãy xem chế độ 'làm tròn đến chẵn gần nhất' hoạt động như thế nào và ý nghĩa của nó trong trường hợp của OP là gì. Hãy xem xét cùng một ví dụ một lần nữa.
67108871 = 00000100_00000000_00000000_00000111
Như chúng ta biết, chúng ta cần 3 dịch chuyển sang phải để đặt 1 ngoài cùng bên trái trong bit # 23:
00000000_1.[0000000_00000000_00000000] 111 * 2^26 (3 bits shifted out)
Quy trình 'làm tròn đến chẵn gần nhất' liên quan đến việc tìm 2 số đóng dấu giá trị đầu vào 67108871
từ dưới cùng trở lên càng gần càng tốt. Hãy nhớ rằng chúng tôi vẫn hoạt động trong FPU trên 80bits vì vậy mặc dù tôi cho thấy một số bit được chuyển ra ngoài chúng vẫn ở trong FPU reg nhưng sẽ bị loại bỏ trong hoạt động làm tròn khi lưu trữ giá trị đầu ra.
00000000_1.[0000000_00000000_00000000] 111 * 2^26 (3 bits shifted out)
2 giá trị gần 00000000_1.[0000000_00000000_00000000] 111 * 2^26
nhau là:
từ đầu:
00000000_1.[0000000_00000000_00000000] 111 * 2^26
+1
= 00000000_1.[0000000_00000000_00000001] * 2^26 = 67108872
và từ bên dưới:
00000000_1.[0000000_00000000_00000000] * 2^26 = 67108864
Rõ ràng 67108872
là gần 67108871
hơn nhiều so với 67108864
do đó chuyển đổi từ giá trị int 32 bit 67108871
mang lại 67108872
(ở chế độ làm tròn thành chẵn gần nhất).
Bây giờ các số của OP (vẫn làm tròn đến chẵn gần nhất):
2147483583 = 01111111_11111111_11111111_10111111
= 00000000_1.[1111111_11111111_11111111] 0111111 * 2^30
giá trị dấu ngoặc:
hàng đầu:
00000000_1.[1111111_111111111_11111111] 0111111 * 2^30
+1
= 00000000_10.[0000000_00000000_00000000] * 2^30
= 00000000_1.[0000000_00000000_00000000] * 2^31 = 2147483648
dưới cùng:
00000000_1.[1111111_111111111_11111111] * 2^30 = 2147483520
Hãy nhớ rằng từ chẵn trong 'làm tròn đến chẵn gần nhất' chỉ quan trọng khi giá trị đầu vào nằm giữa các giá trị trong dấu ngoặc. Chỉ khi đó từ thậm chí mới quan trọng và 'quyết định' giá trị ngoặc nào nên được chọn. Trong trường hợp trên, thậm chí không thành vấn đề và chúng ta chỉ cần chọn giá trị gần hơn, đó là2147483520
Trường hợp của OP cuối cùng cho thấy vấn đề mà ngay cả từ cũng quan trọng. :
2147483584 = 01111111_11111111_11111111_11000000
= 00000000_1.[1111111_11111111_11111111] 1000000 * 2^30
giá trị ngoặc giống như trước đây:
hàng đầu: 00000000_1.[0000000_00000000_00000000] * 2^31 = 2147483648
dưới cùng: 00000000_1.[1111111_111111111_11111111] * 2^30 = 2147483520
Hiện tại không có giá trị nào gần hơn (2147483648-2147483584 = 64 = 2147483584-2147483520) nên chúng ta phải dựa vào giá trị chẵn và chọn giá trị hàng đầu (chẵn) 2147483648
.
Và vấn đề của OP ở đây là Pascal đã mô tả ngắn gọn. FPU chỉ hoạt động trên các giá trị đã ký và 2147483648
không thể được lưu trữ dưới dạng int đã ký vì giá trị tối đa của nó là 2147483647 do đó có vấn đề.
Bằng chứng đơn giản (không cần trích dẫn tài liệu) rằng FPU chỉ hoạt động trên các giá trị đã ký, tức là. xử lý mọi giá trị như đã ký là bằng cách gỡ lỗi này:
unsigned int test = (1u << 31);
_asm
{
fild [test]
}
Mặc dù có vẻ như giá trị thử nghiệm nên được coi là chưa có dấu, nó sẽ được tải là -2 31 vì không có hướng dẫn riêng để tải các giá trị có dấu và chưa dấu vào FPU. Tương tự như vậy, bạn sẽ không tìm thấy các hướng dẫn cho phép bạn lưu trữ giá trị chưa được đánh dấu từ FPU vào mem. Mọi thứ chỉ là một mẫu bit được coi là đã ký bất kể bạn có thể đã khai báo nó như thế nào trong chương trình của mình.
Đã lâu nhưng hy vọng ai đó sẽ học được điều gì đó từ nó.