Cách chính xác để chuyển đổi 2 byte thành số nguyên 16 bit đã ký là gì?


31

Trong câu trả lời này , zwol đã đưa ra yêu cầu này:

Cách chính xác để chuyển đổi hai byte dữ liệu từ nguồn bên ngoài thành số nguyên có chữ ký 16 bit là với các hàm trợ giúp như thế này:

#include <stdint.h>

int16_t be16_to_cpu_signed(const uint8_t data[static 2]) {
    uint32_t val = (((uint32_t)data[0]) << 8) | 
                   (((uint32_t)data[1]) << 0);
    return ((int32_t) val) - 0x10000u;
}

int16_t le16_to_cpu_signed(const uint8_t data[static 2]) {
    uint32_t val = (((uint32_t)data[0]) << 0) | 
                   (((uint32_t)data[1]) << 8);
    return ((int32_t) val) - 0x10000u;
}

Hàm nào trong các hàm trên là phù hợp phụ thuộc vào việc mảng chứa một đại diện endian nhỏ hay đại diện endian lớn. Endianness không phải là vấn đề ở đây, tôi tự hỏi tại sao zwol trừ 0x10000ukhỏi uint32_tgiá trị được chuyển đổi thành int32_t.

Tại sao đây là cách chính xác ?

Làm thế nào để tránh hành vi được xác định thực hiện khi chuyển đổi sang loại trả về?

Vì bạn có thể giả sử đại diện bổ sung của 2, nên diễn viên đơn giản hơn này sẽ thất bại như thế nào: return (uint16_t)val;

Có gì sai với giải pháp ngây thơ này:

int16_t le16_to_cpu_signed(const uint8_t data[static 2]) {
    return (uint16_t)data[0] | ((uint16_t)data[1] << 8);
}

Hành vi chính xác khi truyền tới int16_tđược xác định theo triển khai, vì vậy cách tiếp cận ngây thơ không mang tính di động.
nwellnhof

@nwellnhof không có diễn viên nàoint16_t
MM

Câu hỏi trong tiêu đề không thể được trả lời mà không chỉ định sử dụng ánh xạ nào
MM

4
Cả hai cách tiếp cận đều dựa vào hành vi được xác định thực hiện (chuyển đổi một giá trị không dấu sang loại đã ký không thể biểu thị giá trị). Ví dụ. trong cách tiếp cận thứ nhất, 0xFFFF0001ukhông thể được biểu diễn dưới dạng int16_tvà trong cách tiếp cận thứ hai 0xFFFFukhông thể được biểu diễn dưới dạng int16_t.
Sander De Dycker

1
"Vì bạn có thể giả sử đại diện bổ sung của 2" [cần dẫn nguồn]. C89 và C99 chắc chắn không từ chối các đại diện bổ sung và ký hiệu 1s. Qv, stackoverflow.com/questions/12276957/
Eric Towers

Câu trả lời:


20

Nếu intlà 16 bit thì phiên bản của bạn phụ thuộc vào hành vi được xác định do triển khai nếu giá trị của biểu thức trong returncâu lệnh nằm ngoài phạm vi int16_t.

Tuy nhiên, phiên bản đầu tiên cũng có một vấn đề tương tự; ví dụ nếu int32_tlà một typedef cho intvà các byte đầu vào là cả hai 0xFF, thì kết quả của phép trừ trong câu lệnh return là UINT_MAXnguyên nhân gây ra hành vi được xác định khi thực hiện khi được chuyển đổi thành int16_t.

IMHO câu trả lời bạn liên kết đến có một số vấn đề lớn.


2
Nhưng cách chính xác là gì?
idmean

@idmean câu hỏi cần làm rõ trước khi có thể trả lời, tôi đã yêu cầu trong một bình luận dưới câu hỏi nhưng OP đã không trả lời
MM

1
@MM: Tôi đã chỉnh sửa câu hỏi xác định rằng endianness không phải là vấn đề. IMHO vấn đề zwol đang cố gắng giải quyết là hành vi được xác định khi thực hiện khi chuyển đổi sang loại đích, nhưng tôi đồng ý với bạn: Tôi tin rằng anh ta nhầm vì phương pháp của anh ta có vấn đề khác. Làm thế nào bạn sẽ giải quyết việc thực hiện được xác định hành vi hiệu quả?
chqrlie

@chqrlieforyellowblockquotes Tôi không đề cập cụ thể đến endianness. Bạn có muốn đặt các bit chính xác của hai octet đầu vào int16_tkhông?
MM

@MM: vâng, đó chính xác là câu hỏi. Tôi đã viết byte nhưng từ đúng nên thực sự là octet như kiểu uchar8_t.
chqrlie

7

Điều này phải đúng về mặt giáo dục và cũng hoạt động trên các nền tảng sử dụng các biểu diễn bổ sung bit hoặc 1 , thay vì bổ sung 2 thông thường . Các byte đầu vào được giả sử là bổ sung cho 2.

int le16_to_cpu_signed(const uint8_t data[static 2]) {
    unsigned value = data[0] | ((unsigned)data[1] << 8);
    if (value & 0x8000)
        return -(int)(~value) - 1;
    else
        return value;
}

Vì chi nhánh, nó sẽ đắt hơn các lựa chọn khác.

Điều này đạt được là nó tránh được mọi giả định về cách intđại diện liên quan đến unsignedđại diện trên nền tảng. Việc truyền vào intđược yêu cầu để bảo toàn giá trị số học cho bất kỳ số nào sẽ phù hợp với loại mục tiêu. Bởi vì đảo ngược đảm bảo bit trên cùng của số 16 bit sẽ bằng 0, giá trị sẽ phù hợp. Sau đó, unary -và phép trừ của 1 áp dụng quy tắc thông thường cho phủ định bổ sung của 2. Tùy thuộc vào nền tảng, INT16_MINvẫn có thể tràn nếu nó không phù hợp với intloại trên mục tiêu, trong trường hợp này longnên được sử dụng.

Sự khác biệt so với phiên bản gốc trong câu hỏi đến vào thời gian trả lại. Mặc dù bản gốc chỉ luôn bị trừ 0x10000và phần bù của 2 cho phép tràn tràn ký tên vào int16_tphạm vi, phiên bản này có một ifđiều rõ ràng là tránh sự bao bọc có chữ ký ( không xác định ).

Bây giờ trong thực tế, hầu hết tất cả các nền tảng được sử dụng ngày nay đều sử dụng biểu diễn bổ sung của 2. Trong thực tế, nếu nền tảng có tuân thủ tiêu chuẩnstdint.h xác định int32_t, nó phải sử dụng phần bù 2 cho nó. Cách tiếp cận này đôi khi có ích là với một số ngôn ngữ kịch bản hoàn toàn không có kiểu dữ liệu số nguyên - bạn có thể sửa đổi các thao tác được hiển thị ở trên cho số float và nó sẽ cho kết quả chính xác.


Tiêu chuẩn C đặc biệt bắt buộc rằng int16_tvà bất kỳ intxx_tvà các biến thể không dấu của chúng phải sử dụng biểu diễn bổ sung của 2 mà không cần các bit đệm. Nó sẽ mất một kiến ​​trúc cố ý để lưu trữ các loại này và sử dụng một đại diện khác cho int, nhưng tôi đoán DS9K có thể được cấu hình theo cách này.
chqrlie

@chqrlieforyellowblockquotes Điểm tốt, tôi đã thay đổi để sử dụng intđể tránh nhầm lẫn. Thật vậy, nếu nền tảng xác định int32_tnó phải là phần bù 2.
JPA

Các loại này đã được chuẩn hóa trong C99 theo cách này: C99 7.18.1.1 Các intN_t Nint8_tkiểu số nguyên có chiều rộng chính xác Tên typedef chỉ định một kiểu số nguyên có chữ ký với chiều rộng , không có bit đệm và biểu diễn bổ sung của hai. Do đó, biểu thị một loại số nguyên đã ký với chiều rộng chính xác là 8 bit. Các biểu diễn khác vẫn được hỗ trợ bởi tiêu chuẩn, nhưng đối với các loại số nguyên khác.
chqrlie

Với phiên bản cập nhật của bạn, (int)valuecó hành vi được xác định thực hiện nếu loại intchỉ có 16 bit. Tôi e rằng bạn cần sử dụng (long)value - 0x10000, nhưng trên các kiến ​​trúc bổ sung của 2 không, giá trị 0x8000 - 0x10000không thể được biểu diễn dưới dạng 16 bit int, vì vậy vấn đề vẫn còn.
chqrlie

@chqrlieforyellowblockquotes Vâng, chỉ cần chú ý như vậy, tôi đã sửa với ~ thay vào đó, nhưng longsẽ hoạt động tốt như nhau.
JPA

6

Một phương pháp khác - sử dụng union:

union B2I16
{
   int16_t i;
   byte    b[2];
};

Trong chương trình:

...
B2I16 conv;

conv.b[0] = first_byte;
conv.b[1] = second_byte;
int16_t result = conv.i;

first_bytesecond_bytecó thể được hoán đổi theo mô hình endian nhỏ hoặc lớn. Phương pháp này không tốt hơn nhưng là một trong những lựa chọn thay thế.


2
Không phải là loại hành vi lén lút không xác định hành vi ?
Maxim Egorushkin

1
@MaximEgorushkin: Wikipedia không phải là một nguồn có thẩm quyền để giải thích tiêu chuẩn C.
Eric Postpischil

2
@EricPostpischil Tập trung vào tin nhắn hơn là tin nhắn là không khôn ngoan.
Maxim Egorushkin

1
@MaximEgorushkin: oh vâng, rất tiếc tôi đã đọc sai nhận xét của bạn. Giả sử byte[2]int16_tcó cùng kích thước, nó là một hoặc một trong hai thứ tự có thể, không phải là một số giá trị được đặt xáo trộn theo bit bit tùy ý. Vì vậy, ít nhất bạn có thể phát hiện tại thời điểm biên dịch những gì mà endianity thực hiện.
Peter Cordes

1
Tiêu chuẩn nêu rõ rằng giá trị của thành viên công đoàn là kết quả của việc diễn giải các bit được lưu trữ trong thành viên dưới dạng đại diện giá trị của loại đó. Có các khía cạnh được xác định theo thực thi, trong đó việc biểu diễn các kiểu được xác định theo triển khai.
MM

6

Các toán tử số học thay đổibitwise - hoặc trong biểu thức (uint16_t)data[0] | ((uint16_t)data[1] << 8)không hoạt động trên các loại nhỏ hơn int, để các uint16_tgiá trị đó được thăng cấp lên int(hoặc unsignednếu sizeof(uint16_t) == sizeof(int)). Tuy nhiên, điều đó sẽ mang lại câu trả lời đúng, vì chỉ có 2 byte thấp hơn chứa giá trị.

Một phiên bản chính xác khác về mặt giáo dục cho chuyển đổi từ cuối lớn sang cuối nhỏ (giả sử CPU ít endian) là:

#include <string.h>
#include <stdint.h>

int16_t be16_to_cpu_signed(const uint8_t data[2]) {
    int16_t r;
    memcpy(&r, data, sizeof r);
    return __builtin_bswap16(r);
}

memcpyđược sử dụng để sao chép đại diện int16_tvà đó là cách tuân thủ tiêu chuẩn để làm như vậy. Phiên bản này cũng biên dịch thành 1 hướng dẫn movbe, xem phần lắp ráp .


1
@MM Một lý do __builtin_bswap16tồn tại là vì hoán đổi byte trong ISO C không thể được thực hiện một cách hiệu quả.
Maxim Egorushkin

1
Không đúng; trình biên dịch có thể phát hiện ra rằng mã thực hiện hoán đổi byte và dịch nó như là một nội dung hiệu quả
MM

1
Chuyển đổi int16_tthành uint16_tđược xác định rõ: giá trị âm chuyển đổi thành giá trị lớn hơn INT_MAX, nhưng chuyển đổi các giá trị này trở lại uint16_tlà hành vi được xác định thực hiện: 6.3.1.3 Số nguyên đã ký và không dấu 1. Khi một giá trị có loại số nguyên được chuyển đổi sang loại số nguyên khác ngoài_Bool, nếu giá trị có thể được đại diện bởi loại mới, nó không thay đổi. ... 3. Mặt khác, loại mới được ký và giá trị không thể được biểu thị trong đó; hoặc kết quả là xác định thực hiện hoặc tín hiệu xác định thực hiện được đưa ra.
chqrlie

1
@MaximEgorushkin gcc dường như không hoạt động tốt trong phiên bản 16 bit, nhưng clang tạo cùng mã cho ntohs/ __builtin_bswap|/ <<mẫu: gcc.godbolt.org/z/rJ-j87
PSkocik

3
@MM: Tôi nghĩ Maxim đang nói "không thể thực hành với trình biên dịch hiện tại". Tất nhiên, một trình biên dịch không thể hút một lần và nhận ra việc tải các byte liền kề vào một số nguyên. GCC7 hoặc 8 cuối cùng đã giới thiệu lại tải / lưu trữ kết hợp lại cho các trường hợp không cần đảo ngược byte , sau khi GCC3 bỏ nó hàng thập kỷ trước. Nhưng trong các trình biên dịch nói chung có xu hướng cần trợ giúp trong thực tế với rất nhiều thứ mà CPU có thể làm một cách hiệu quả nhưng điều mà ISO C đã bỏ qua / từ chối phơi bày một cách hợp lý. ISO C di động không phải là ngôn ngữ tốt để thao tác bit / byte mã hiệu quả.
Peter Cordes

4

Đây là một phiên bản khác chỉ dựa trên các hành vi di động và được xác định rõ (tiêu đề #include <endian.h>không chuẩn, mã là):

#include <endian.h>
#include <stdint.h>
#include <string.h>

static inline void swap(uint8_t* a, uint8_t* b) {
    uint8_t t = *a;
    *a = *b;
    *b = t;
}
static inline void reverse(uint8_t* data, int data_len) {
    for(int i = 0, j = data_len / 2; i < j; ++i)
        swap(data + i, data + data_len - 1 - i);
}

int16_t be16_to_cpu_signed(const uint8_t data[2]) {
    int16_t r;
#if __BYTE_ORDER == __LITTLE_ENDIAN
    uint8_t data2[sizeof r];
    memcpy(data2, data, sizeof data2);
    reverse(data2, sizeof data2);
    memcpy(&r, data2, sizeof r);
#else
    memcpy(&r, data, sizeof r);
#endif
    return r;
}

Phiên bản ít endian biên dịch thành một movbelệnh với clang, gccphiên bản ít tối ưu hơn, xem phần lắp ráp .


@chqrlieforyellowblockquotes mối quan tâm chính của bạn có vẻ như đã được uint16_tđể int16_tchuyển đổi, phiên bản này không có chuyển đổi, vì vậy ở đây bạn đi.
Maxim Egorushkin

2

Tôi muốn cảm ơn tất cả những người đóng góp cho câu trả lời của họ. Đây là những gì các công trình tập thể nắm bắt được:

  1. Theo C Chuẩn 7.20.1.1 loại nguyên Exact-width : các loại uint8_t, int16_tuint16_t phải sử dụng đại diện bổ sung hai mà không cần bất kỳ bit đệm, vì vậy các bit thực tế của các đại diện được một cách rõ ràng những người của 2 byte trong mảng, theo thứ tự theo quy định của tên hàm.
  2. tính toán giá trị 16 bit không dấu với (unsigned)data[0] | ((unsigned)data[1] << 8) (đối với phiên bản cuối nhỏ) biên dịch thành một lệnh đơn và mang lại giá trị 16 bit không dấu.
  3. Theo tiêu chuẩn C 6.3.1.3 Số nguyên đã ký và chưa ký : chuyển đổi giá trị của loại uint16_tthành loại đã kýint16_t có hành vi được xác định nếu giá trị không nằm trong phạm vi của loại đích. Không có quy định đặc biệt nào được thực hiện cho các loại có đại diện được xác định chính xác.
  4. để tránh hành vi được xác định thực hiện này, người ta có thể kiểm tra xem giá trị không dấu lớn hơn INT_MAXvà tính giá trị đã ký tương ứng bằng cách trừ đi 0x10000. Làm điều này cho tất cả các giá trị theo đề xuất của zwol có thể tạo ra các giá trị ngoài phạm vi int16_tcó cùng hành vi được xác định thực hiện.
  5. kiểm tra 0x8000bit rõ ràng làm cho trình biên dịch tạo mã không hiệu quả.
  6. một chuyển đổi hiệu quả hơn mà không thực hiện hành vi được xác định sử dụng kiểu pucky thông qua liên minh, nhưng cuộc tranh luận về tính xác định của phương pháp này vẫn còn bỏ ngỏ, ngay cả ở cấp Ủy ban của C Standard.
  7. loại picky có thể được thực hiện một cách hợp lý và với hành vi được xác định bằng cách sử dụng memcpy.

Kết hợp các điểm 2 và 7, đây là một giải pháp di động và được xác định đầy đủ, biên dịch hiệu quả thành một lệnh duy nhất với cả gccclang :

#include <stdint.h>
#include <string.h>

int16_t be16_to_cpu_signed(const uint8_t data[2]) {
    int16_t r;
    uint16_t u = (unsigned)data[1] | ((unsigned)data[0] << 8);
    memcpy(&r, &u, sizeof r);
    return r;
}

int16_t le16_to_cpu_signed(const uint8_t data[2]) {
    int16_t r;
    uint16_t u = (unsigned)data[0] | ((unsigned)data[1] << 8);
    memcpy(&r, &u, sizeof r);
    return r;
}

Hội 64-bit :

be16_to_cpu_signed(unsigned char const*):
        movbe   ax, WORD PTR [rdi]
        ret
le16_to_cpu_signed(unsigned char const*):
        movzx   eax, WORD PTR [rdi]
        ret

Tôi không phải là một luật sư ngôn ngữ, nhưng chỉ charcác loại có thể bí danh hoặc chứa đại diện đối tượng của bất kỳ loại nào khác. uint16_tkhông phải là một trong charcác loại, do đó memcpycủa uint16_tđể int16_tkhông phải là hành vi được xác định rõ. Tiêu chuẩn chỉ yêu cầu char[sizeof(T)] -> T > char[sizeof(T)]chuyển đổi memcpyđể được xác định rõ.
Maxim Egorushkin

memcpycủa uint16_tđể int16_tlà thực hiện được quy định tại tốt nhất, không di động, không rõ ràng, chính xác như phân công một đến khác, và bạn không thể kỳ diệu tránh né điều đó với memcpy. Không quan trọng việc uint16_tsử dụng biểu diễn bổ sung của hai hay không, hoặc các bit đệm có hay không - đó không phải là hành vi được xác định hoặc yêu cầu theo tiêu chuẩn C.
Maxim Egorushkin

Với rất nhiều từ, bạn "giải pháp" nắm để thay thế r = ucho memcpy(&r, &u, sizeof u)nhưng sau này không tốt hơn so với trước đây, phải không?
Maxim Egorushkin
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.