Truyền kép thành int unsigned trên Win32 đang bị cắt bớt xuống còn 2.147.483.648


86

Biên dịch mã sau:

double getDouble()
{
    double value = 2147483649.0;
    return value;
}

int main()
{
     printf("INT_MAX: %u\n", INT_MAX);
     printf("UINT_MAX: %u\n", UINT_MAX);

     printf("Double value: %f\n", getDouble());
     printf("Direct cast value: %u\n", (unsigned int) getDouble());
     double d = getDouble();
     printf("Indirect cast value: %u\n", (unsigned int) d);

     return 0;
}

Đầu ra (MSVC x86):

INT_MAX: 2147483647
UINT_MAX: 4294967295
Double value: 2147483649.000000
Direct cast value: 2147483648
Indirect cast value: 2147483649

Đầu ra (MSVC x64):

INT_MAX: 2147483647
UINT_MAX: 4294967295
Double value: 2147483649.000000
Direct cast value: 2147483649
Indirect cast value: 2147483649

Trong tài liệu của Microsoft không đề cập đến giá trị tối đa của số nguyên có dấu trong các chuyển đổi từ doublesang unsigned int.

Tất cả các giá trị trên INT_MAXđang bị cắt bớt 2147483648khi nó là giá trị trả về của một hàm.

Tôi đang sử dụng Visual Studio 2019 để xây dựng chương trình. Điều này không xảy ra trên gcc .

Tôi có đang làm sai không? Có cách nào an toàn để chuyển đổi doublesang unsigned intkhông?


24
Và không, bạn không làm gì sai (có lẽ ngoài việc cố gắng sử dụng trình biên dịch "C" của Microsoft)
Antti Haapala

5
Hoạt động trên máy của tôi ™, được thử nghiệm trên VS2017 v15.9.18 và VS2019 v16.4.1. Sử dụng Trợ giúp> Gửi phản hồi> Báo cáo lỗi để cho họ biết về phiên bản của bạn.
Hans Passant

5
Tôi có thể tái tạo, tôi có kết quả tương tự như kết quả của OP. VS2019 16.7.3.
anastaciu

2
@EricPostpischil thực sự, đó là mô hình bit củaINT_MIN
Antti Haapala

Câu trả lời:


71

Một lỗi trình biên dịch ...

Từ hợp ngữ được cung cấp bởi @anastaciu, các cuộc gọi mã truyền trực tiếp __ftol2_sse, dường như chuyển đổi số thành một ký tự dài . Tên thường lệ là ftol2_ssevì đây là một máy hỗ trợ sse - nhưng float nằm trong một thanh ghi dấu chấm động x87.

; Line 17
    call    _getDouble
    call    __ftol2_sse
    push    eax
    push    OFFSET ??_C@_0BH@GDLBDFEH@Direct?5cast?5value?3?5?$CFu?6@
    call    _printf
    add esp, 8

Mặt khác, dàn diễn viên gián tiếp không

; Line 18
    call    _getDouble
    fstp    QWORD PTR _d$[ebp]
; Line 19
    movsd   xmm0, QWORD PTR _d$[ebp]
    call    __dtoui3
    push    eax
    push    OFFSET ??_C@_0BJ@HCKMOBHF@Indirect?5cast?5value?3?5?$CFu?6@
    call    _printf
    add esp, 8

bật và lưu trữ giá trị kép vào biến cục bộ, sau đó tải nó vào một thanh ghi SSE và gọi __dtoui3đó là quy trình chuyển đổi int kép sang không dấu ...

Hành vi của dàn diễn viên trực tiếp không tuân theo C89; cũng như không tuân theo bất kỳ bản sửa đổi nào sau này - ngay cả C89 cũng nói rõ ràng rằng:

Thao tác kết xuất được thực hiện khi một giá trị của kiểu tích phân được chuyển đổi thành kiểu không dấu không cần thực hiện khi một giá trị của kiểu nổi được chuyển thành kiểu không dấu. Do đó, phạm vi giá trị di động là [0, Utype_MAX + 1) .


Tôi tin rằng vấn đề có thể là sự tiếp diễn của điều này từ năm 2005 - đã từng có một hàm chuyển đổi được gọi là hàm __ftol2có thể hoạt động đối với mã này, tức là nó sẽ chuyển đổi giá trị thành một số có dấu -2147483647, điều này sẽ tạo ra đúng kết quả khi giải thích một số không có dấu.

Thật không may, __ftol2_ssenó không phải là một thay thế thả vào __ftol2, vì nó sẽ - thay vì chỉ lấy các bit giá trị ít quan trọng nhất như hiện tại - báo hiệu lỗi ngoài phạm vi bằng cách trả về LONG_MIN/ 0x80000000, được hiểu là không được ký lâu ở đây không phải là tất cả những gì đã được mong đợi. Hành vi của __ftol2_ssesẽ hợp lệ signed long, vì chuyển đổi giá trị kép> LONG_MAXthành signed longsẽ có hành vi không xác định.


23

Theo câu trả lời của @ AnttiHaapala , tôi đã kiểm tra mã bằng cách sử dụng tối ưu hóa /Oxvà nhận thấy rằng điều này sẽ loại bỏ lỗi __ftol2_ssekhông còn được sử dụng:

//; 17   :     printf("Direct cast value: %u\n", (unsigned int)getDouble());

    push    -2147483647             //; 80000001H
    push    OFFSET $SG10116
    call    _printf

//; 18   :     double d = getDouble();
//; 19   :     printf("Indirect cast value: %u\n", (unsigned int)d);

    push    -2147483647             //; 80000001H
    push    OFFSET $SG10117
    call    _printf
    add esp, 28                 //; 0000001cH

Các tối ưu hóa nội tuyến getdouble()và thêm đánh giá biểu thức không đổi, do đó loại bỏ nhu cầu chuyển đổi trong thời gian chạy, làm cho lỗi biến mất.

Chỉ vì tò mò, tôi đã thực hiện thêm một số thử nghiệm, cụ thể là thay đổi mã để buộc chuyển đổi float-to-int trong thời gian chạy. Trong trường hợp này, kết quả vẫn chính xác, trình biên dịch, với tối ưu hóa, sử dụng __dtoui3trong cả hai chuyển đổi:

//; 19   :     printf("Direct cast value: %u\n", (unsigned int)getDouble(d));

    movsd   xmm0, QWORD PTR _d$[esp+24]
    add esp, 12                 //; 0000000cH
    call    __dtoui3
    push    eax
    push    OFFSET $SG9261
    call    _printf

//; 20   :     double db = getDouble(d);
//; 21   :     printf("Indirect cast value: %u\n", (unsigned int)db);

    movsd   xmm0, QWORD PTR _d$[esp+20]
    add esp, 8
    call    __dtoui3
    push    eax
    push    OFFSET $SG9262
    call    _printf

Tuy nhiên, việc ngăn chặn nội tuyến, __declspec(noinline) double getDouble(){...}sẽ đưa lỗi trở lại:

//; 17   :     printf("Direct cast value: %u\n", (unsigned int)getDouble(d));

    movsd   xmm0, QWORD PTR _d$[esp+76]
    add esp, 4
    movsd   QWORD PTR [esp], xmm0
    call    _getDouble
    call    __ftol2_sse
    push    eax
    push    OFFSET $SG9261
    call    _printf

//; 18   :     double db = getDouble(d);

    movsd   xmm0, QWORD PTR _d$[esp+80]
    add esp, 8
    movsd   QWORD PTR [esp], xmm0
    call    _getDouble

//; 19   :     printf("Indirect cast value: %u\n", (unsigned int)db);

    call    __ftol2_sse
    push    eax
    push    OFFSET $SG9262
    call    _printf

__ftol2_sseđược gọi trong cả hai chuyển đổi tạo ra kết quả 2147483648trong cả hai tình huống, nghi ngờ @zwol là đúng.


Chi tiết biên dịch:

  • Sử dụng dòng lệnh:
cl /permissive- /GS /analyze- /W3 /Gm- /Ox /sdl /D "WIN32" program.c        
  • Trong Visual Studio:

    • Vô hiệu hóa RTCtrong Project -> Properties -> Code Generationvà thiết lập cơ bản Runtime Kiểm tra để mặc định .

    • Bật tối ưu hóa trong Project -> Properties -> Optimizationvà đặt Tối ưu hóa thành / Ox .

    • Với trình gỡ lỗi trong x86chế độ.


5
Funny cách họ như "ok với tối ưu hóa kích hoạt, hành vi undefined sẽ được thực sự không xác định" => mã thực sự hoạt động một cách chính xác: F
Antti Haapala

3
@AnttiHaapala, vâng, vâng, Microsoft tốt nhất.
anastaciu

1
Các tối ưu hóa được áp dụng là nội tuyến và sau đó là đánh giá biểu thức liên tục. Nó không thực hiện chuyển đổi float sang int trong thời gian chạy nữa. Tôi tự hỏi liệu lỗi có quay trở lại nếu bạn buộc getDoublevượt quá dòng và / hoặc thay đổi nó để trả về giá trị mà trình biên dịch không thể chứng minh là không đổi hay không.
zwol

1
@zwol, bạn đã đúng, việc buộc đánh giá ngoài luồng và ngăn chặn đánh giá liên tục sẽ khiến lỗi quay trở lại, nhưng lần này là ở cả hai chuyển đổi.
anastaciu

7

Không ai đã nhìn vào asm cho MS __ftol2_sse.

Từ kết quả, chúng tôi có thể suy ra rằng nó có thể đã chuyển đổi từ x87 thành có dấu int/ long(cả hai loại 32-bit trên Windows), thay vì một cách an toàn uint32_t.

x86 FP -> lệnh số nguyên làm tràn kết quả số nguyên không chỉ quấn / cắt ngắn: chúng tạo ra cái mà Intel gọi là "số nguyên không xác định" khi giá trị chính xác không thể biểu diễn trong đích: tập bit cao, các bit khác rõ ràng. tức là0x80000000 .

(Hoặc nếu ngoại lệ không hợp lệ FP không bị che, nó sẽ kích hoạt và không có giá trị nào được lưu trữ. Nhưng trong môi trường FP mặc định, tất cả các ngoại lệ FP đều bị che. Đó là lý do tại sao đối với các phép tính FP, bạn có thể nhận được NaN thay vì lỗi.)

Điều đó bao gồm cả hướng dẫn x87 như fistp(sử dụng chế độ làm tròn hiện tại) và hướng dẫn SSE2 như cvttsd2si eax, xmm0(sử dụng cắt bớt về 0, đó là ý tnghĩa bổ sung ).

Vì vậy, đó là một lỗi để biên dịch double-> unsignedchuyển đổi thành cuộc gọi đến __ftol2_sse.


Ghi chú bên lề / ốp:

Trên x86-64, FP -> uint32_t có thể được biên dịch sang cvttsd2si rax, xmm0, chuyển đổi thành đích có dấu 64-bit, tạo ra uint32_t bạn muốn ở nửa thấp (EAX) của đích số nguyên.

Đó là C và C ++ UB nếu kết quả nằm ngoài phạm vi 0..2 ^ 32-1, do đó, các giá trị âm hoặc dương rất lớn sẽ để lại nửa thấp của RAX (EAX) bằng 0 từ mẫu bit không xác định số nguyên. (Không giống như chuyển đổi số nguyên-> số nguyên, việc giảm mô-đun giá trị không được đảm bảo. Hành vi truyền một giá trị kép âm sang int không dấu có được xác định trong tiêu chuẩn C không? Hành vi khác nhau trên ARM so với x86 . Nói rõ ràng là không có gì trong câu hỏi là hành vi chưa được xác định hoặc thậm chí do triển khai xác định. Tôi chỉ chỉ ra rằng nếu bạn có FP-> int64_t, bạn có thể sử dụng nó để triển khai hiệu quả FP-> uint32_t. Điều đó bao gồm x87fistp có thể ghi đích đến số nguyên 64 bit ngay cả ở chế độ 32 bit và 16 bit, không giống như các lệnh SSE2 chỉ có thể xử lý trực tiếp số nguyên 64 bit ở chế độ 64 bit.


1
Tôi sẽ bị cám dỗ để xem mã đó nhưng may mắn thay tôi không có MSVC ...: D
Antti Haapala

@AnttiHaapala: Vâng, tôi cũng vậy
Peter Cordes
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.