2016-11-25 17 views
12

Đoạn mã sau (C99 và mới hơn) muốn tính một hình vuông, bị giới hạn ở cùng một số bit như loại chiều rộng cố định ban đầu.Làm thế nào để buộc số học chưa ký trên các loại chiều rộng cố định?

#include <stdint.h> 
    uint8_t sqr8(uint8_t x) { return x*x; } 
    uint16_t sqr16(uint16_t x) { return x*x; } 
    uint32_t sqr32(uint32_t x) { return x*x; } 
    uint64_t sqr64(uint64_t x) { return x*x; } 

Vấn đề là: tùy thuộc vào kích thước int, một số các phép nhân có thể được thực hiện trên đối số thăng (đã ký) int, với kết quả tràn một (đã ký) int, kết quả như vậy, không xác định như xa như tiêu chuẩn là có liên quan ; và kết quả sai lầm có thể tưởng tượng được, đặc biệt là trên các máy (ngày càng hiếm) không sử dụng two's complement.

Nếu int là 32-bit (resp. 16-bit, 64-bit, 80 hoặc 128-bit), xảy ra cho sqr16 (resp. sqr8, sqr32, sqr64) khi x0xFFFFF (resp. 0xFF, 0xFFFFFFFF, 0xFFFFFFFFFFFFFFFF). Cả 4 chức năng đều không thể di chuyển theo C99 !!

C11 hoặc mới hơn, hoặc một số ấn bản C++, khắc phục tình huống không may đó?


A, giải pháp làm việc đơn giản là:

#include <stdint.h> 
    uint8_t sqr8(uint8_t x) { return 1u*x*x; } 
    uint16_t sqr16(uint16_t x) { return 1u*x*x; } 
    uint32_t sqr32(uint32_t x) { return 1u*x*x; } 
    uint64_t sqr64(uint64_t x) { return 1u*x*x; } 

Đây là tiêu chuẩn tuân thủ QTI vì 1u không thăng int và vẫn unsigned; do đó phép nhân bên trái, sau đó là phép nhân bên phải, được thực hiện dưới dạng unsigned, do đó được xác định rõ để mang lại kết quả chính xác trong số lượng bit có thứ tự thấp cần thiết; tương tự cho các diễn viên tiềm ẩn cuối cùng với chiều rộng kết quả.

Cập nhật: Như đề nghị trong comment by Marc Glisse, tôi đã cố gắng biến thể này với tám biên dịch (ba phiên bản của GCC cho x86 bắt đầu với 3.1, MS C/C++ 19.00, Keil ARM trình biên dịch 5, hai trình biên dịch vũ trụ cho các biến thể ST7, Microchip MCC18). Tất cả chúng đều tạo ra cùng một mã như bản gốc (với các tối ưu tôi sử dụng trong chế độ phát hành cho các dự án thực tế). Tuy nhiên, các trình biên dịch có thể tạo ra mã tồi tệ hơn so với bản gốc; và tôi có một số trình biên dịch nhúng khác của tôi để thử, bao gồm một số trình biên dịch 68K và PowerPC.

Chúng tôi có những tùy chọn nào khác, tạo sự cân bằng hợp lý giữa khả năng hoạt động tốt hơn, dễ đọc và đơn giản hơn?

+2

Cast rộng hơn cho phép nhân, sau đó đúc trở lại kiểu hẹp 'uint8_t sqr8 (uint8_t x) {return (uint8_t) ((uint16_t) x * (uint16_t) x); } ' – Toby

+1

Bạn nói đúng về quan sát chung: các bí danh loại không tách rời có kích thước cố định không phù hợp cho các phép tính số học với hành vi mô-đun. Sử dụng 'unsigned long' hoặc một cái gì đó như thế. Điều quan trọng là bạn cần phải kiểm soát xếp hạng chuyển đổi của loại và các bí danh có kích thước chứa thông tin xếp hạng chuyển đổi * không *. Thậm chí 'uintmax_t' có thể là một bí danh cho' unsigned char'. –

+3

@Toby: sự hiểu biết của tôi là ngay cả với các phôi, các đối số sẽ * vẫn * được thăng thành 'int'. – Bathsheba

Trả lời

5

Bạn đã xác định thời gian ngắn cơ bản của bí danh loại số nguyên trong <stdint.h>: Chúng không chứa bất kỳ thông tin nào về thứ hạng chuyển đổi của loại. Do đó, bạn không kiểm soát được liệu các giá trị của các loại đó có trải qua các chương trình khuyến mãi không tách rời hay không và khi bạn quan sát chính xác, biểu thức có thể có hành vi không xác định khi kết quả thăng hạng kết quả theo loại đã ký.

Tóm lại: bạn không thể sử dụng các loại bí danh cho mục đích thực hiện các phép tính số học thông thường modulo 2 N. Bạn cần sử dụng loại có xếp hạng chuyển đổi (được biết!) Ít nhất là loại có số int. Giải pháp nói chung sẽ là chuyển đổi toán hạng của bạn thành thích hợp nhỏ nhất của unsigned int, unsigned long int hoặc unsigned long long int (miễn là nền tảng của bạn không có loại tích phân mở rộng), sau đó đánh giá biểu thức và sau đó chuyển về kiểu gốc (có hành vi mô-đun đúng). Trong C++, bạn có thể viết một đặc điểm kiểu mà tìm ra đúng loại theo cách di động.

Là một mẹo rẻ hơn, và một lần nữa giả định rằng không có các loại tích phân mở rộng, bạn chỉ có thể quảng cáo mọi thứ đến unsigned long long int và hy vọng trình biên dịch của bạn thực hiện tính toán một cách hiệu quả.

+0

Tôi rất muốn thấy _ "đặc điểm kiểu C++" _ thành ngữ! – fgrieu

+0

Trong C++, bạn thậm chí không nhất thiết phải có một đặc điểm kiểu: nó có thể được viết bằng văn bản như là 'decltype (1u * x)' (trong đó phép nhân không bao giờ được thực thi). – hvd

+1

@KerrekSB Đó * là * loại không dấu. – hvd

4

Bạn không thể tránh quảng bá loại không thể tránh khỏi thành int cho các loại không dấu hẹp hơn.

Đó là một thuộc tính của toán tử phép nhân hơn bất kỳ thứ gì khác.

Để tránh trường hợp hành vi góc không xác định, các chỉ điều bạn có thể làm là không bao giờ sử dụng nhân khi sử dụng các loại unsigned nơi vuông tối đa của chúng có thể tràn int.

May mắn (trừ khi bạn đang làm việc trong thế giới nhúng được bạn luôn có thể tham khảo tài liệu cho các hành vi chính xác), bạn có thể phần lớn uỷ thác unsigned short lịch sử: intunsigned anh em họ của nó rất có thể sẽ không có chậm hơn, và có thể nhanh hơn .

+0

Phải; Tôi đang yêu cầu _how_ làm điều này. Bao gồm, sử dụng một phiên bản tốt hơn của định nghĩa ngôn ngữ; hoặc một số thành ngữ hợp lý và hiệu quả. Thành ngữ '1u *' khá hợp lý, và hoàn toàn tuân thủ các tiêu chuẩn (theo sự hiểu biết của tôi), nhưng thất bại trong bài kiểm tra _efficient_, với nhiều trình biên dịch. – fgrieu

+0

Bạn không thể. Điều đó là không thể. Đó là những gì tôi đang cố * nói. – Bathsheba

0

Làm cách nào để buộc số học không dấu trên các loại chiều rộng cố định?
Chúng tôi có những tùy chọn nào khác, ...?

Bằng cách sử dụng loại chiều rộng cố định tối thiểu là unsigned cho loại đối số của hàm.

Điều này làm cho việc chuyển đổi thành ít nhất unsigned như là một phần của thông số chuyển. Các loại tham số chính thức và các kiểu trả về vẫn là kiểu "chiều rộng cố định" cổ điển. Các tăng cường thực tế của hàm cũng là các kiểu chiều rộng cố định, nhưng có thể là các kiểu chiều rộng cố định rộng hơn.

#if UINT16_MAX >= UINT_MAX 
    typedef uint8 uint16_t 
    typedef uint16 uint16_t 
    typedef uint32 uint32_t 
    typedef uint64 uint64_t 
#elif UINT32_MAX >= UINT_MAX 
    typedef uint8 uint32_t 
    typedef uint16 uint32_t 
    typedef uint32 uint32_t 
    typedef uint64 uint64_t 
#elif UINT64_MAX >= UINT_MAX 
    typedef uint8 uint64_t 
    typedef uint16 uint64_t 
    typedef uint32 uint64_t 
    typedef uint64 uint64_t 
#endif 

uint16_t sqr16(uint16 x) { return x*x; } 
uint16_t sqr32(uint32 x) { return x*x; } 
uint16_t sqr64(uint64 x) { return x*x; } 

// usage 
uint16_t x16 = ...; 
uint32_t x32 = ...; 
uint64_t x64 = ...; 
x16 = sqr16(x16); 
x32 = sqr32(x32); 
x64 = sqr64(x64); 

Tùy thuộc vào chức năng, đây là vấn đề mặc dù chức năng được gọi với kiểu rộng hơn như dưới đây. uint16_t foo16(uint16 x) có thể không có biện pháp phòng ngừa để nhận giá trị ngoài phạm vi uint16_t.

x16 = foo16(x32); 

Nếu tất cả điều này tốt hơn? Tôi vẫn thích rõ ràng 1u như trong

uint16_t sqr16(uint16_t x) { return 1u*x*x; } 
Các vấn đề liên quan