2 baytı işaretli bir 16 bit tam sayıya dönüştürmenin doğru yolu nedir?


31

Gelen bu cevap , Zwol bu iddiayı yaptı:

İki bayt veriyi harici bir kaynaktan 16 bit işaretli tam sayıya dönüştürmenin doğru yolu aşağıdaki gibi yardımcı işlevlerdir:

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

Yukarıdaki işlevlerden hangisinin uygun olduğu, dizinin küçük bir endian mı yoksa büyük bir endian temsili içeriğine mi bağlıdır. Endian neden merak ediyorum, burada söz konu değil Zwol toplayıp çıkarmadan 0x10000ugelen uint32_tdeğere dönüştürülür int32_t.

Neden doğru yol bu ?

Dönüş türüne dönüştürürken uygulama tanımlı davranışı nasıl önler?

2'nin tamamlayıcı temsilini varsayabildiğiniz için, bu daha basit yayın nasıl başarısız olur: return (uint16_t)val;

Bu saf çözümün nesi yanlış:

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

Döküm yaparken tam davranış int16_tuygulama tanımlıdır, bu yüzden saf yaklaşım taşınabilir değildir.
nwellnhof

@nwellnhof oyuncu kadrosu yokint16_t
MM

Başlıktaki soru, hangi eşlemenin kullanılacağını belirtmeden cevaplanamaz
MM

4
Her iki yaklaşım da uygulama tanımlı davranışa dayanır (imzasız bir değeri, değeri temsil edemeyen işaretli bir türe dönüştürme). Örneğin. ilk yaklaşımda, 0xFFFF0001uolarak temsil edilemez int16_tve ikinci yaklaşımda 0xFFFFuolarak temsil edilemez int16_t.
Sander De Dycker

1
"2'nin tamamlayıcı temsilini kabul edebildiğiniz için" [alıntı gerekli]. C89 ve C99 kesinlikle 1'in tamamlayıcı ve işaret büyüklükteki temsillerini reddetmedi. Qv, stackoverflow.com/questions/12276957/…
Eric Towers

Yanıtlar:


20

Eğer int16-bit daha sonra ifade değeri eğer sürüm uygulama tanımlı davranışları dayanır returnifadesi için aralık dışında int16_t.

Ancak ilk versiyonun da benzer bir sorunu var; örneğin int32_t, bir typedef ise intve girdi baytlarının her ikisi de 0xFFise, return ifadesindeki çıkarma sonucu, UINT_MAXdönüştürüldüğünde uygulama tanımlı davranışa neden olur int16_t.

İMHO'ya verdiğiniz cevabın birkaç önemli sorunu var.


2
Peki doğru yol nedir?
idmean

@idmean cevaplanmadan önce sorunun açıklığa kavuşturulması gerekiyor, ben sorunun altındaki bir yorumda talep ettim ama OP cevap vermedi
MM

1
@MM: Endianness'in sorun olmadığını belirtmek için soruyu düzenledim. IMHO zwol çözmeye çalıştığı sorun hedef türüne dönüştürürken uygulama tanımlı davranıştır, ama ben size katılıyorum: onun yöntemi başka sorunları var gibi yanlış olduğuna inanıyorum. Uygulama tanımlı davranışı verimli bir şekilde nasıl çözersiniz?
chqrlie

@chqrlieforyellowblockquotes Özellikle endianiteye değinmedim. Sadece iki giriş oktetinin tam bitlerini int16_t?
MM

@MM: evet, soru bu. Bayt yazdım ama doğru kelime türü gibi sekizli olmalı uchar8_t.
chqrlie

7

Bu bilgiçlikle doğru olmalı ve normal 2'nin tamamlayıcısı yerine işaret biti veya 1'in tamamlayıcı gösterimlerini kullanan platformlarda da çalışmalıdır . Giriş baytlarının 2'nin tamamlayıcısı içinde olduğu varsayılır.

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

Şube nedeniyle, diğer seçeneklerden daha pahalı olacaktır.

Bunun başardığı şey, temsilin platformdaki temsil ile nasıl intilişkili olduğu konusunda herhangi bir varsayımdan kaçınmasıdır unsigned. intHedef türüne sığacak herhangi bir sayının aritmetik değerini korumak için oyuncu kadrosu gereklidir. Ters çevirme, 16 bitlik sayının üst bitinin sıfır olmasını sağladığından değer sığar. Daha sonra -1'in tekli ve çıkarılması, 2'nin tamamlayıcı olumsuzlaması için olağan kuralı uygular. Platforma bağlı olarak , hedef üzerindeki tipe INT16_MINuymazsa yine de taşabilir int, bu durumda longkullanılmalıdır.

Sorudaki orijinal versiyonun farkı dönüş zamanında gelir. Orijinal her zaman çıkarılır 0x10000ve 2'nin tamamlayıcısı, imzalı taşmanın onu int16_taralığa ifsarmasına izin verirken , bu sürümde imzalı sarmalamayı ( tanımsız olan ) önleyen açıktır .

Şimdi pratikte, bugün kullanılan neredeyse tüm platformlar 2'nin tamamlayıcı temsilini kullanmaktadır. Aslında, platformun stdint.htanımlayan standart uyumlu olması durumunda int32_t, bunun için 2'nin tamamlayıcısını kullanması gerekir . Bu yaklaşımın bazen kullanışlı olduğu yerlerde, tamsayı veri türlerine sahip olmayan bazı kodlama dillerinde kullanılır - yukarıda yüzen işlemler için gösterilen işlemleri değiştirebilirsiniz ve doğru sonucu verecektir.


Bu Standart C, özellikle zorunlu int16_tve herhangi bir intxx_tve işaretsiz varyantları dolgu bitleri olmadan 2'ye tümleme temsil kullanmalıdır. Bu türleri barındırmak ve başka bir gösterim kullanmak bilerek sapkın bir mimari alacaktı int, ama sanırım DS9K bu şekilde yapılandırılabilir.
chqrlie

@chqrlieforyellowblockquotes İyi bir nokta, intkarışıklığı önlemek için kullanmak için değişti . Gerçekten de platform tanımlıysa int32_t2'nin tamamlayıcısı olmalıdır.
jpa

Bu türler C99'da şu şekilde standartlaştırılmıştır: C99 7.18.1.1 Tam genişlikli tamsayı türleri typedef adı intN_t , genişlik N, dolgu biti ve ikisinin tamamlayıcı gösterimi olan işaretli bir tamsayı türünü belirtir . Böylece, int8_tgenişliği tam olarak 8 bit olan işaretli bir tamsayı türünü belirtir. Diğer temsiller hala standart tarafından desteklenmektedir, ancak diğer tamsayı türleri için.
chqrlie

Güncellenmiş sürümünüzde, (int)valuetürde intyalnızca 16 bit varsa uygulama tanımlı davranışı vardır. Korkarım kullanmanız gerekiyor (long)value - 0x10000, ancak 2'nin tamamlayıcı mimarilerinde değer 0x8000 - 0x1000016 bit olarak temsil edilemiyor int, bu yüzden sorun devam ediyor.
chqrlie

@chqrlieforyellowblockquotes Evet, sadece fark ettim, bunun yerine ~ ile sabitledim, ama longeşit derecede iyi çalışırdı .
jpa

6

Başka bir yöntem - kullanma union:

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

Programda:

...
B2I16 conv;

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

first_byteve second_byteküçük veya büyük endian modeline göre değiştirilebilir. Bu yöntem daha iyi değil, alternatiflerden biridir.



1
@MaximEgorushkin: Wikipedia, C standardını yorumlamak için yetkili bir kaynak değildir.
Eric Postpischil

2
@EricPostpischil Mesaj yerine mesajcıya odaklanmak mantıksız.
Maxim Egorushkin

1
@MaximEgorushkin: oh evet, oops yorumunuzu yanlış okudum. Varsayarsak byte[2]ve int16_taynı boyutta olan, bu bir veya iki olası sıralamaların diğer değil, keyfi bir bit düzeyinde yer değerlerini karıştırdı. Böylece en azından uygulamanın hangi endianiteye sahip olduğunu derleyebilirsiniz.
Peter Cordes

1
Standart, sendika üyesinin değerinin, üye içinde depolanan bitleri, bu tür bir değer temsili olarak yorumlamanın sonucu olduğunu açıkça belirtir. Uygulamaların tanımlı yönleri vardır, türlerin gösterimi uygulama tanımlıdır.
MM

6

Aritmetik işleçler kaydırılır ve bitsel olarak veya ifade olarak (uint16_t)data[0] | ((uint16_t)data[1] << 8)daha küçük türlerde çalışmaz int, bu nedenle bu uint16_tdeğerler yükselir int(veya unsignedif sizeof(uint16_t) == sizeof(int)). Yine de, doğru cevabı vermelidir, çünkü sadece düşük 2 bayt değeri içerir.

Big-endian'dan little-endian dönüşümüne (little-endian CPU varsayalım) başka pedantically doğru sürüm:

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

memcpytemsilini kopyalamak için kullanılır int16_tve bunun standart uyumlu bir yoludur. Bu sürüm ayrıca 1 talimata da derlenmiştir movbe, bkz. Montaj .


1
@MM Bunun bir nedeni __builtin_bswap16, ISO C'deki bayt değişiminin verimli bir şekilde uygulanamamasıdır.
Maxim Egorushkin

1
Doğru değil; derleyici, kodun bayt değişimini uyguladığını algılayabilir ve verimli bir yerleşik olarak çevirebilir
MM

1
Dönüştürme int16_tiçin uint16_tde tanımlanır: negatif değerler daha yüksek değerlere dönüştürmek INT_MAX, ama geri Bu değerler dönüştürülmesi uint16_t: Uygulama tanımlanan davranıştır 6.3.1.3 işaretli ve işaretsiz tamsayı tamsayı tip bir değer başka bir tamsayı türü diğer than_Bool dönüştürüldüğünde ise, 1. değer yeni türle temsil edilebilir, değişmez. ... 3. Aksi takdirde, yeni tür imzalanır ve değer içinde temsil edilemez; sonuç uygulama tarafından tanımlanır veya uygulama tarafından tanımlanan bir sinyal oluşturulur.
chqrlie

1
@MaximEgorushkin gcc 16-bit sürümünde çok iyi görünmüyor, ancak clang ntohs/ __builtin_bswapve |/ <<pattern için aynı kodu üretir : gcc.godbolt.org/z/rJ-j87
PSkocik

3
@MM: Bence Maxim " güncel derleyicilerle pratik yapamaz " diyor. Elbette bir derleyici bir kez ememez ve bitişik baytların bir tamsayıya yüklenmesini tanıyamaz. GCC7 veya 8 nihayet yaptım yeniden tanıtmak bayt ters durumlar için kaynaştırma yük / mağaza değil GCC3 onlarca yıl önce düşürdü sonra, gerekli. Ancak genel olarak derleyiciler, CPU'ların verimli bir şekilde yapabileceği, ancak ISO C'nin portatif olarak açığa vurmayı ihmal ettiği / reddettiği birçok şey için pratikte yardıma ihtiyaç duymaktadır. Taşınabilir ISO C, verimli kod biti / bayt manipülasyonu için iyi bir dil değildir.
Peter Cordes

4

Yalnızca taşınabilir ve iyi tanımlanmış davranışlara dayanan başka bir sürüm (başlık #include <endian.h>standart değil, kod):

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

Little-endian versiyonu tek bir movbetalimatla derlenir clang, gccversiyon daha az optimumdur, bkz. Montaj .


Ana endişe olmuş gibi görünmektedir @chqrlieforyellowblockquotes uint16_tiçin int16_tbu sürüm, işte gidip, o dönüşümü yoktur, dönüşüm.
Maxim Egorushkin

2

Tüm katılımcılara cevapları için teşekkür etmek istiyorum. Kolektif eserlerin kaygıları şöyledir:

  1. Cı standardına uygun olarak, 7.20.1.1 Tam genişlikli tamsayı türleri : türleri uint8_t, int16_tve uint16_ttemsili gerçek bit yani belirtilen sırayla, dizideki açıkça, herhangi bir dolgu bitleri olmadan 2 bayt bu ikinin tümleyici temsil kullanmalıdır işlev adları.
  2. işaretsiz 16 bit değerini (unsigned)data[0] | ((unsigned)data[1] << 8)(küçük endian sürümü için) hesaplamak tek bir komutu derler ve işaretsiz 16 bitlik bir değer verir.
  3. C Standardı 6.3.1.3 İmzalı ve imzasız tamsayılara göre : bir tür değerinin uint16_tişaretli türe dönüştürülmesi, değer int16_thedef türünde değilse uygulama tanımlı davranışa sahiptir. Temsili kesin olarak tanımlanmış tipler için özel bir hüküm yoktur.
  4. bu uygulama tanımlı davranışı önlemek için, imzasız değerin büyük olup olmadığını test edebilir INT_MAXve çıkararak karşılık gelen işaretli değeri hesaplayabilirsiniz 0x10000. Bunu zwol tarafından önerildiği gibi tüm değerler için yapmak int16_t, aynı uygulama tanımlı davranışa sahip aralığın dışında değerler üretebilir .
  5. 0x8000bit için test yapılması derleyicilerin verimsiz kod üretmesine neden olur.
  6. uygulama tanımlı davranışı olmadan daha verimli bir dönüşüm , sendika aracılığıyla kurnaz tip kullanır , ancak bu yaklaşımın tanımlanmasına ilişkin tartışma, C Standardı Komite düzeyinde bile açıktır.
  7. türü Cinaslar portably gerçekleştirilir kullanılarak davranışı tarif ile olabilir memcpy.

Noktaları 2 ve 7 bir araya burada, taşınabilir ve tam olarak tanımlanmış bir çözüm her iki tek bir talimat verimli derlenir gcc ve clang :

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

64 bitlik Montaj :

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

Ben bir dil avukatı değilim, ancak yalnızca chartürler diğer türlerin nesne temsilini içerebilir veya nesne gösterimini içerebilir. uint16_tşunlardan biri değildir charki, türleri memcpyarasında uint16_tiçin int16_tiyi tanımlanmış davranış değildir. Standart yalnızca char[sizeof(T)] -> T > char[sizeof(T)]dönüşümün memcpyiyi tanımlanmasını gerektirir .
Maxim Egorushkin

memcpyarasında uint16_thiç int16_ttam olarak birinden diğerine atama olarak, en iyi şekilde iyi tanımlanmış değil, taşınabilir değil uygulama tanımlı, ve o sihirli hileli olamaz memcpy. uint16_tİkisinin tamamlayıcı temsilini kullanıp kullanmadığının veya doldurma bitlerinin mevcut olup olmadığı önemli değildir - bu C standardı tarafından tanımlanan veya gerekli olmayan davranış değildir.
Maxim Egorushkin

Her türlü deyişle ile, "çözüm" yerine aşağı kaynar r = uiçin memcpy(&r, &u, sizeof u)ancak ikincisi, daha iyi eski daha değil mi?
Maxim Egorushkin
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.