Apa cara yang benar untuk mengonversi 2 byte ke integer 16-bit yang sudah ditandatangani?

31

Dalam jawaban ini , zwol membuat klaim ini:

Cara yang benar untuk mengkonversi dua byte data dari sumber eksternal menjadi integer bertanda 16-bit adalah dengan fungsi pembantu seperti ini:

#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;
}

Manakah dari fungsi di atas yang sesuai tergantung pada apakah array berisi representasi endian kecil atau besar. Endianness bukan masalah yang dipertanyakan di sini, saya bertanya-tanya mengapa zwol mengurangi 0x10000udari uint32_tnilai yang dikonversi int32_t.

Mengapa ini cara yang benar ?

Bagaimana cara menghindari perilaku yang ditentukan implementasi ketika mengkonversi ke tipe kembali?

Karena Anda dapat mengasumsikan representasi komplemen 2's, bagaimana gips sederhana ini gagal: return (uint16_t)val;

Apa yang salah dengan solusi naif ini:

int16_t le16_to_cpu_signed(const uint8_t data[static 2]) {
    return (uint16_t)data[0] | ((uint16_t)data[1] << 8);
}
chqrlie
sumber
Perilaku yang tepat saat melakukan casting int16_tdidefinisikan, sehingga pendekatan naif tidak mudah dibawa-bawa.
nwellnhof
@nwellnhof tidak ada casting untukint16_t
MM
Pertanyaan dalam judul tidak dapat dijawab tanpa menentukan pemetaan yang akan digunakan
MM
4
Kedua pendekatan bergantung pada implementasi perilaku yang didefinisikan (mengkonversi nilai yang tidak ditandatangani ke tipe yang ditandatangani yang tidak dapat mewakili nilai). Misalnya. dalam pendekatan pertama, 0xFFFF0001utidak dapat direpresentasikan sebagai int16_t, dan dalam pendekatan kedua 0xFFFFutidak dapat direpresentasikan sebagai int16_t.
Sander De Dycker
1
"Karena Anda dapat menganggap representasi komplemen 2's" [rujukan?]. C89 dan C99 tentu tidak menyangkal representasi 1s dan sign-magnitude. Qv, stackoverflow.com/questions/12276957/…
Eric Towers

Jawaban:

20

Jika int16-bit maka versi Anda bergantung pada perilaku yang ditentukan implementasi jika nilai ekspresi dalam returnpernyataan di luar kisaran int16_t.

Namun versi pertama juga memiliki masalah serupa; misalnya jika int32_tadalah typedef untuk int, dan byte input keduanya 0xFF, maka hasil pengurangan dalam pernyataan kembali adalah UINT_MAXyang menyebabkan perilaku yang ditentukan implementasi ketika dikonversi ke int16_t.

IMHO jawaban yang Anda tautkan memiliki beberapa masalah besar.

MM
sumber
2
Tapi apa cara yang benar?
idmean
@ id berarti pertanyaan perlu klarifikasi sebelum dapat dijawab, saya telah meminta komentar di bawah pertanyaan tetapi OP belum menjawab
MM
1
@ MM: Saya mengedit pertanyaan yang menentukan bahwa endianness bukan masalah. IMHO, masalah yang coba diselesaikan oleh zwol adalah penerapan perilaku yang ditentukan saat mengonversi ke tipe tujuan, tapi saya setuju dengan Anda: Saya yakin dia salah karena metodenya memiliki masalah lain. Bagaimana Anda memecahkan implementasi perilaku yang didefinisikan secara efisien?
chqrlie
@ chqrlieforyellowblockquotes Saya tidak mengacu pada endianness secara khusus. Apakah Anda hanya ingin memasukkan bit tepat dari dua oktet input ke dalam int16_t?
MM
@ MM: ya, itulah pertanyaannya. Saya menulis byte tetapi kata yang benar memang harus oktet seperti tipenya uchar8_t.
chqrlie
7

Ini harus benar pedantically dan bekerja juga pada platform yang menggunakan bit tanda atau representasi komplemen 1 , alih-alih pelengkap 2 yang biasa . Input byte dianggap sebagai komplemen 2's.

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;
}

Karena cabang, itu akan lebih mahal daripada opsi lain.

Apa yang dicapai ini adalah bahwa ia menghindari asumsi tentang bagaimana intketerwakilan berhubunganunsigned representasi pada platform. Para pemain intdiharuskan untuk mempertahankan nilai aritmatika untuk nomor apa pun yang sesuai dengan tipe target. Karena inversi memastikan bit top dari angka 16-bit akan menjadi nol, nilainya akan pas. Kemudian unary -dan pengurangan 1 menerapkan aturan biasa untuk negasi komplemen 2's. Tergantung pada platform, INT16_MINmasih bisa meluap jika tidak sesuai dengan inttipe pada target, dalam hal ini longharus digunakan.

Perbedaan ke versi asli dalam pertanyaan muncul di waktu pengembalian. Sementara yang asli selalu dikurangkan 0x10000dan komplemen 2's membiarkan overflow yang ditandatangani membungkusnya ke int16_tkisaran, versi ini memiliki eksplisit ifyang menghindari wrapover yang ditandatangani (yang tidak ditentukan ).

Sekarang dalam praktiknya, hampir semua platform yang digunakan saat ini menggunakan representasi komplemen 2's. Bahkan, jika platform memiliki standar-standar stdint.hyang mendefinisikan int32_t, itu harus menggunakan komplemen 2 untuk itu. Di mana pendekatan ini kadang-kadang berguna adalah dengan beberapa bahasa skrip yang tidak memiliki tipe data integer sama sekali - Anda dapat memodifikasi operasi yang ditunjukkan di atas untuk float dan itu akan memberikan hasil yang benar.

jpa
sumber
Standar C secara khusus mengamanatkan bahwa int16_tdan setiap intxx_tdan varian yang tidak ditandatangani harus menggunakan representasi komplemen 2 tanpa bit padding. Ini akan mengambil arsitektur yang sengaja disalahgunakan untuk meng-host jenis ini dan menggunakan representasi lain untuk int, tapi saya kira DS9K dapat dikonfigurasi dengan cara ini.
chqrlie
@ chqrlieforyellowblockquotes Poin bagus, saya mengubah penggunaan intuntuk menghindari kebingungan. Memang jika platform mendefinisikan int32_titu harus 2 komplemen.
jpa
Tipe-tipe ini distandarisasi dalam C99 dengan cara ini: C99 7.18.1.1 Tipe integer lebar persis Nama typedef intN_t menunjuk tipe integer bertanda dengan lebar N, tanpa bit bantalan, dan representasi komplemen dua. Jadi, int8_tmenunjukkan tipe integer yang ditandatangani dengan lebar tepat 8 bit. Representasi lain masih didukung oleh standar, tetapi untuk tipe integer lainnya.
chqrlie
Dengan versi Anda yang diperbarui, (int)valueimplementasikan perilaku yang ditentukan jika tipe inthanya memiliki 16 bit. Saya khawatir Anda perlu menggunakan (long)value - 0x10000, tetapi pada arsitektur pelengkap non 2, nilainya 0x8000 - 0x10000tidak dapat direpresentasikan sebagai 16-bit int, sehingga masalahnya tetap ada.
chqrlie
@ chqrlieforyellowblockquotes Ya, hanya memperhatikan hal yang sama, saya perbaiki dengan ~ sebagai gantinya, tetapi longakan bekerja sama baiknya.
jpa
6

Metode lain - menggunakan union:

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

Dalam program:

...
B2I16 conv;

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

first_bytedan second_bytedapat ditukar sesuai dengan model endian kecil atau besar. Metode ini tidak lebih baik tetapi merupakan salah satu alternatif.

i486
sumber
2
Bukankah tipe serikat pekerja menghukum perilaku yang tidak ditentukan ?
Maxim Egorushkin
1
@ MaximEgorushkin: Wikipedia bukan sumber otoritatif untuk menafsirkan standar C.
Eric Postpischil
2
@EricPostpischil Berfokus pada messenger daripada pesannya tidak bijaksana.
Maxim Egorushkin
1
@ MaximEgorushkin: oh ya, oops, saya salah membaca komentar Anda. Dengan asumsi byte[2]dan int16_tukuran yang sama, itu adalah satu atau yang lain dari dua kemungkinan pemesanan, bukan beberapa nilai tempat bitwise shuffled sewenang-wenang. Jadi, Anda setidaknya dapat mendeteksi pada waktu kompilasi berapa endianness implementasi.
Peter Cordes
1
Standar tersebut dengan jelas menyatakan bahwa nilai anggota serikat adalah hasil dari menafsirkan bit yang disimpan dalam anggota sebagai representasi nilai dari jenis itu. Ada aspek-aspek implementasi-didefinisikan sejauh representasi tipe didefinisikan-implementasi.
MM
6

Operator aritmatika bergeser dan bitwise-atau dalam ekspresi (uint16_t)data[0] | ((uint16_t)data[1] << 8)tidak bekerja pada tipe yang lebih kecil dari int, sehingga uint16_tnilai - nilai tersebut dipromosikan ke int(atau unsignedjikasizeof(uint16_t) == sizeof(int) ). Meski begitu, itu harus menghasilkan jawaban yang benar, karena hanya 2 byte yang lebih rendah yang mengandung nilai.

Versi pedantically lain yang benar untuk konversi big-endian ke little-endian (dengan asumsi little-endian CPU) adalah:

#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);
}

memcpydigunakan untuk menyalin representasi int16_tdan itu adalah cara yang sesuai standar untuk melakukannya. Versi ini juga mengkompilasi menjadi 1 instruksi movbe, lihat perakitan .

Maxim Egorushkin
sumber
1
@ MM Salah satu alasannya __builtin_bswap16adalah karena byte-swapping di ISO C tidak dapat diimplementasikan secara efisien.
Maxim Egorushkin
1
Tidak benar; kompiler dapat mendeteksi bahwa kode mengimplementasikan byte swapping dan menerjemahkannya sebagai builtin yang efisien
MM
1
Mengubah int16_tmenjadi uint16_tterdefinisi dengan baik: nilai negatif dikonversi ke nilai lebih besar dari INT_MAX, tetapi mengubah nilai-nilai ini kembali ke uint16_tperilaku implementasi yang didefinisikan: 6.3.1.3 Bilangan bulat bertanda dan tidak bertanda 1. Ketika nilai dengan tipe integer dikonversi ke tipe integer lain selain than_Bool, jika nilai dapat diwakili oleh tipe baru, itu tidak berubah. ... 3. Jika tidak, tipe baru ditandatangani dan nilainya tidak dapat diwakili di dalamnya; baik hasilnya adalah implementasi yang ditentukan atau sinyal implementasi yang ditetapkan dinaikkan.
chqrlie
1
@MaximEgorushkin gcc tampaknya tidak begitu baik dalam versi 16-bit, tetapi dentang menghasilkan kode yang sama untuk ntohs/ __builtin_bswapdan |/ <<pola: gcc.godbolt.org/z/rJ-j87
PSkocik
3
@ MM: Saya pikir Maxim mengatakan "tidak bisa dalam praktek dengan kompiler saat ini". Tentu saja seorang kompiler tidak dapat menyedot sekali dan mengenali pemuatan byte yang berdekatan ke dalam integer. GCC7 atau 8 akhirnya memperkenalkan kembali load / store penggabungan untuk kasus-kasus di mana byte-reverse tidak diperlukan, setelah GCC3 menjatuhkannya beberapa dekade yang lalu. Tetapi pada umumnya kompiler cenderung membutuhkan bantuan dalam praktiknya dengan banyak hal yang dapat dilakukan CPU secara efisien tetapi ISO C diabaikan / ditolak untuk diekspos secara mudah. ISO C portabel bukan bahasa yang baik untuk manipulasi kode bit / byte yang efisien.
Peter Cordes
4

Berikut adalah versi lain yang hanya bergantung pada perilaku portabel dan terdefinisi dengan baik (header #include <endian.h>bukan standar, kodenya):

#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;
}

Versi little-endian mengkompilasi ke movbeinstruksi tunggal dengan clang, gccversi kurang optimal, lihat perakitan .

Maxim Egorushkin
sumber
@chqrlieforyellowblockquotes perhatian utama Anda tampaknya telah uint16_tke int16_tkonversi, versi ini tidak memiliki konversi itu, jadi di sini Anda pergi.
Maxim Egorushkin
2

Saya ingin mengucapkan terima kasih kepada semua kontributor atas jawaban mereka. Inilah yang menjadi tujuan kerja kolektif:

  1. Sesuai dengan Standar C 7.20.1.1 Tipe integer lebar-lebar : tipe uint8_t, int16_tdan uint16_tharus menggunakan representasi komplemen dua tanpa bit padding, sehingga bit sebenarnya dari representasi secara jelas dari 2 byte dalam array, dalam urutan yang ditentukan oleh nama fungsi.
  2. menghitung nilai 16 bit yang tidak ditandatangani dengan (unsigned)data[0] | ((unsigned)data[1] << 8) (untuk versi endian kecil) mengkompilasi ke instruksi tunggal dan menghasilkan nilai 16-bit yang tidak ditandatangani.
  3. Sesuai dengan Standar C 6.3.1.3 Bilangan bulat yang ditandatangani dan tidak ditandatangani : mengonversi nilai tipe uint16_tke tipe yang ditandatangani int16_tmemiliki perilaku implementasi yang ditetapkan jika nilainya tidak dalam kisaran tipe tujuan. Tidak ada ketentuan khusus yang dibuat untuk jenis yang perwakilannya didefinisikan secara tepat.
  4. untuk menghindari implementasi perilaku yang didefinisikan ini, seseorang dapat menguji apakah nilai yang tidak ditandatangani lebih besar dari INT_MAXdan menghitung nilai yang ditandatangani dengan mengurangi 0x10000. Melakukan ini untuk semua nilai seperti yang disarankan oleh zwol dapat menghasilkan nilai di luar rentang int16_tdengan perilaku yang didefinisikan implementasi yang sama.
  5. menguji 0x8000bit secara eksplisit menyebabkan kompiler menghasilkan kode yang tidak efisien.
  6. konversi yang lebih efisien tanpa implementasi perilaku yang didefinisikan menggunakan jenis hukuman melalui serikat pekerja, tetapi perdebatan mengenai definisi pendekatan ini masih terbuka, bahkan di tingkat Komite Standar C.
  7. type punning dapat dilakukan dengan mudah dan dengan perilaku yang jelas menggunakan memcpy.

Menggabungkan poin 2 dan 7, berikut ini adalah solusi portabel dan terdefinisi penuh yang mengkompilasi secara efisien ke satu instruksi dengan gcc dan dentang :

#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;
}

Perakitan 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
chqrlie
sumber
Saya bukan pengacara bahasa, tetapi hanya chartipe yang bisa alias atau mengandung representasi objek dari jenis apa pun. uint16_tbukan salah satu dari charjenis, sehingga memcpydari uint16_tuntuk int16_ttidak perilaku didefinisikan dengan baik. Standar hanya memerlukan char[sizeof(T)] -> T > char[sizeof(T)]konversi dengan memcpyharus didefinisikan dengan baik.
Maxim Egorushkin
memcpyof uint16_tto int16_tadalah implementasi yang didefinisikan terbaik, tidak portabel, tidak terdefinisi dengan baik, persis seperti penugasan satu ke yang lain, dan Anda tidak dapat secara ajaib mengelaknya denganmemcpy . Tidak masalah apakah uint16_tmenggunakan representasi komplemen dua atau tidak, atau bit padding ada atau tidak - itu bukan perilaku yang ditentukan atau diminta oleh standar C.
Maxim Egorushkin
Dengan begitu banyak kata-kata, Anda "solusi" bermuara mengganti r = uke memcpy(&r, &u, sizeof u)tapi yang terakhir ini tidak lebih baik dari yang pertama, bukan?
Maxim Egorushkin