Helpex - Trao đổi & giúp đỡ Đăng nhập
42

Hãy xem xét đoạn mã sau, đây là một SSCCE của sự cố thực tế của tôi:

#include <iostream>

int roundtrip(int x)
{
    return int(float(x));
}

int main()
{
    int a = 2147483583;
    int b = 2147483584;
    std::cout << a << " -> " << roundtrip(a) << '\n';
    std::cout << b << " -> " << roundtrip(b) << '\n';
}

Đầu ra trên máy tính của tôi (Xubuntu 12.04.3 LTS) là:

2147483583 -> 2147483520
2147483584 -> -2147483648

Lưu ý cách số dương bkết thúc bằng số âm sau khi làm tròn. Hành vi này có được chỉ định rõ không? Tôi đã mong đợi việc vấp vòng int-to-float để ít nhất giữ đúng dấu hiệu ...

Hm, trên Ideone , đầu ra khác:

2147483583 -> 2147483520
2147483584 -> 2147483647

Nhóm g ++ đã sửa lỗi trong thời gian chờ đợi hay cả hai kết quả đầu ra đều hoàn toàn hợp lệ?

42 hữu ích 4 bình luận 5.5k xem chia sẻ
68

Chương trình của bạn đang gọi hành vi không xác định do có lỗi tràn trong quá trình chuyển đổi từ dấu phẩy động sang số nguyên. Những gì bạn thấy chỉ là hiện tượng thông thường trên bộ vi xử lý x86.

Các floatgiá trị gần 21474835842 31 chính xác (việc chuyển đổi từ số nguyên để dấu chấm động thường vòng đến gần nhất, mà có thể lên, và trong trường hợp này. Để cụ thể, hành vi khi chuyển đổi từ số nguyên để dấu chấm động được do thực thi định nghĩa, hầu hết các triển khai định nghĩa làm tròn là “theo chế độ làm tròn FPU” và chế độ làm tròn mặc định của FPU là làm tròn đến gần nhất).

Sau đó, trong khi chuyển đổi từ float đại diện cho 2 31 thành int, một lỗi tràn sẽ xảy ra. Phần tràn này là hành vi không xác định. Một số bộ xử lý đưa ra một ngoại lệ, những bộ khác thì bão hòa. Lệnh IA-32 cvttsd2sithường được tạo bởi các trình biên dịch luôn trả về INT_MINtrong trường hợp bị tràn, bất kể số float là tích cực hay tiêu cực.

Bạn không nên dựa vào hành vi này ngay cả khi bạn biết mình đang nhắm mục tiêu bộ xử lý Intel: khi nhắm mục tiêu x86-64, trình biên dịch có thể phát ra, để chuyển đổi từ dấu phẩy động sang số nguyên, chuỗi hướng dẫn tận dụng hành vi không xác định để trả về kết quả khác với những gì bạn có thể mong đợi đối với kiểu số nguyên đích .

68 hữu ích 5 bình luận chia sẻ
10

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 - fstphơ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 2147483583thà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 67108872hoàn toàn có thể biểu diễn bằng float 32bit và sẽ tốt hơn nhiều 67108864phả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 67108871từ 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^26nhau 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 67108872là gần 67108871hơn nhiều so với 67108864do đó chuyển đổi từ giá trị int 32 bit 67108871mang 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à 2147483648khô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ó.

10 hữu ích 2 bình luận chia sẻ
loading
Không tìm thấy câu trả lời bạn tìm kiếm? Duyệt qua các câu hỏi được gắn thẻ c++ floating-point type-conversion ieee-754 twos-complement , hoặc hỏi câu hỏi của bạn.

Có thể bạn quan tâm

loading