วิธีที่ถูกต้องในการแปลง 2 ไบต์เป็นจำนวนเต็ม 16 บิตที่ลงนามคืออะไร?


31

ในคำตอบนี้ , zwolทำให้การเรียกร้องนี้:

วิธีที่ถูกต้องในการแปลงข้อมูลสองไบต์จากแหล่งข้อมูลภายนอกให้เป็นจำนวนเต็ม 16 บิตที่มีลายเซ็นคือฟังก์ชันผู้ช่วยเช่นนี้

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

ฟังก์ชันข้างต้นใดที่เหมาะสมขึ้นอยู่กับว่าอาเรย์นั้นประกอบไปด้วย endian เพียงเล็กน้อยหรือการแสดง endian ขนาดใหญ่ endianness ไม่เป็นปัญหาที่คำถามที่นี่ผมกำลังสงสัยว่าทำไมzwolหัก0x10000uจากค่าแปลงuint32_tint32_t

เหตุใดจึงเป็นวิธีที่ถูกต้อง ?

มันจะหลีกเลี่ยงพฤติกรรมที่กำหนดโดยการนำไปปฏิบัติเมื่อแปลงเป็นชนิดส่งคืนได้อย่างไร

เนื่องจากคุณสามารถสันนิษฐานได้ว่าการเป็นตัวแทน 2 นั้นจะทำให้การโยนง่ายขึ้นอย่างไร: return (uint16_t)val;

เกิดอะไรขึ้นกับโซลูชันไร้เดียงสานี้:

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

พฤติกรรมที่แน่นอนเมื่อนำไปint16_tใช้เป็นการกำหนดการใช้งานดังนั้นวิธีการที่ไร้เดียงสาจึงไม่สามารถพกพาได้
nwellnhof

@nwellnhof ไม่มีการส่งถึงint16_t
MM

ไม่สามารถตอบคำถามในชื่อโดยไม่ระบุการแมปที่จะใช้
MM

4
ทั้งสองวิธีขึ้นอยู่กับพฤติกรรมที่กำหนดไว้ในการใช้งาน (การแปลงค่าที่ไม่ได้ลงชื่อเป็นประเภทที่ลงชื่อแล้วซึ่งไม่สามารถแทนค่าได้) เช่น. ในวิธีแรก0xFFFF0001uไม่สามารถแทนได้int16_tและในวิธีที่สอง0xFFFFuจะไม่สามารถแทนint16_tได้
Sander De Dycker

1
"เมื่อคุณสามารถสันนิษฐานได้ว่าเป็นตัวแทนของ 2" [อ้างจำเป็น] แน่นอนว่า C89 และ C99 ไม่ได้ปฏิเสธการเป็นตัวแทน 1S ที่สมบูรณ์และเป็นตัวแทนของขนาดสัญญาณ Qv, stackoverflow.com/questions/12276957/…
Eric Towers

คำตอบ:


20

ถ้าintเป็นแบบ 16 บิตแล้วรุ่นที่คุณอาศัยอยู่กับพฤติกรรมการดำเนินงานที่กำหนดไว้ถ้าค่าของการแสดงออกในที่คำสั่งจะออกช่วงสำหรับreturnint16_t

อย่างไรก็ตามรุ่นแรกยังมีปัญหาที่คล้ายกัน เช่นถ้าint32_tเป็น typedef หาintและไบต์การป้อนข้อมูลทั้งสอง0xFFแล้วผลของการลบในงบผลตอบแทนที่เป็นซึ่งเป็นสาเหตุของพฤติกรรมการดำเนินงานที่กำหนดไว้เมื่อแปลงเป็นUINT_MAXint16_t

IMHO คำตอบที่คุณเชื่อมโยงไปมีปัญหาสำคัญหลายประการ


2
แต่วิธีที่ถูกต้องคืออะไร?
idmean

@idmean คำถามต้องมีการชี้แจงก่อนที่จะสามารถตอบได้ฉันได้ขอความคิดเห็นภายใต้คำถาม แต่ OP ไม่ได้ตอบ
MM

1
@MM: ฉันแก้ไขคำถามระบุว่า endianness ไม่ใช่ปัญหา IMHO ปัญหาที่ zwol พยายามแก้ไขคือพฤติกรรมที่กำหนดไว้ในการนำไปใช้เมื่อแปลงเป็นประเภทปลายทาง แต่ฉันเห็นด้วยกับคุณ: ฉันเชื่อว่าเขาเข้าใจผิดเนื่องจากวิธีการของเขามีปัญหาอื่น ๆ คุณจะแก้ไขพฤติกรรมการปฏิบัติที่กำหนดไว้ได้อย่างมีประสิทธิภาพอย่างไร
chqrlie

@chqrlieforyellowblockquotes ฉันไม่ได้อ้างถึง endianness โดยเฉพาะ คุณต้องการใส่บิตที่แน่นอนของสองอ็อกเท็ตอินพุตในint16_t?
MM

@MM: ใช่นั่นเป็นคำถาม ผมเขียนไบต์แต่คำที่ถูกต้องแน่นอนควรจะoctetsuchar8_tเป็นชนิดคือ
chqrlie

7

นี้ควรจะอวดที่ถูกต้องและการทำงานยังบนแพลตฟอร์มที่ใช้บิตเครื่องหมายหรือ1 ส่วนเติมเต็มการแสดง, แทนที่จะเป็น2 ส่วนเติมเต็ม ไบต์อินพุตจะถือว่าเป็นส่วนเสริมของ 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;
}

เนื่องจากสาขาจะมีราคาแพงกว่าตัวเลือกอื่น ๆ

สิ่งนี้ประสบความสำเร็จคือมันหลีกเลี่ยงข้อสันนิษฐานใด ๆ ว่าการintเป็นตัวแทนเกี่ยวข้องกับการนำunsignedเสนอบนแพลตฟอร์มอย่างไร intจำเป็นต้องใช้cast เพื่อเก็บค่าเลขคณิตสำหรับหมายเลขใด ๆ ที่จะพอดีกับประเภทเป้าหมาย เนื่องจากการผกผันทำให้จำนวนบิตบนสุดของ 16 บิตเป็นศูนย์ค่าจะพอดี จากนั้นการรวมกันเป็นหนึ่ง-และการลบของ 1 จะใช้กฎปกติสำหรับการปฏิเสธแบบเติมเต็มของ 2 ขึ้นอยู่กับแพลตฟอร์มINT16_MINอาจยังล้นได้หากไม่เหมาะกับintประเภทของเป้าหมายซึ่งlongควรใช้ในกรณีใด

ความแตกต่างของเวอร์ชันต้นฉบับในคำถามนั้นมาจากเวลาที่ส่งคืน ในขณะที่เดิมเพียงแค่ลบออกเสมอ0x10000และ 2 ส่วนเติมเต็มให้ลงนามห่อล้นไปยังint16_tช่วงรุ่นนี้มีความชัดเจนifที่หลีกเลี่ยงการลงนาม wrapover (ซึ่งไม่ได้กำหนด )

ตอนนี้ในทางปฏิบัติเกือบทุกแพลตฟอร์มที่ใช้อยู่ในปัจจุบันใช้ตัวแทนที่สมบูรณ์ของ 2 ในความเป็นจริงหากแพลตฟอร์มมีมาตรฐานstdint.hที่กำหนดไว้int32_tมันจะต้องใช้ส่วนประกอบ 2 ของมัน วิธีการที่สะดวกในบางครั้งวิธีนี้คือภาษาสคริปต์บางตัวที่ไม่มีชนิดข้อมูลจำนวนเต็มเลย - คุณสามารถแก้ไขการดำเนินการที่แสดงด้านบนเพื่อทำการลอยได้และจะให้ผลลัพธ์ที่ถูกต้อง


มาตรฐาน C สั่งทำหน้าที่เฉพาะเจาะจงว่าint16_tและintxx_tตัวแปรใด ๆและไม่ได้ลงนามของพวกเขาจะต้องใช้การเป็นตัวแทนของส่วนเสริม 2 โดยไม่มีการเติมบิต มันจะใช้สถาปัตยกรรมแบบวิปริตโดยเจตนาเพื่อโฮสต์ประเภทเหล่านี้และใช้การเป็นตัวแทนอื่นสำหรับintแต่ฉันคิดว่า DS9K สามารถกำหนดค่าด้วยวิธีนี้
chqrlie

@chqrlieforyellowblockquotes จุดที่ดีฉันเปลี่ยนไปใช้intเพื่อหลีกเลี่ยงความสับสน แน่นอนถ้าแพลตฟอร์มกำหนดint32_tว่ามันจะต้องเป็นส่วนประกอบของ 2
jpa

ประเภทเหล่านี้ได้รับการมาตรฐานใน C99 ด้วยวิธีนี้: C99 7.18.1.1 ประเภทจำนวนเต็มความกว้างที่แน่นอน ชื่อ typedef intN_t กำหนดประเภทจำนวนเต็มลงนามที่มีความกว้างNไม่มีบิตint8_tแพ็ดดิ้งและเป็นตัวแทนของทั้งสอง ดังนั้นจะระบุประเภทจำนวนเต็มที่ลงนามแล้วซึ่งมีความกว้างเท่ากับ 8 บิต การรับรองอื่น ๆ ยังคงได้รับการสนับสนุนโดยมาตรฐาน แต่สำหรับประเภทจำนวนเต็มอื่น ๆ
chqrlie

ด้วยเวอร์ชันที่อัปเดตของคุณ(int)valueจะมีพฤติกรรมการใช้งานที่กำหนดหากประเภทintมีเพียง 16 บิต ฉันเกรงว่าคุณจะต้องใช้(long)value - 0x10000แต่ในสถาปัตยกรรมประกอบที่ไม่ใช่ของ 2 ค่า0x8000 - 0x10000ไม่สามารถแสดงเป็น 16 บิตintดังนั้นปัญหายังคงอยู่
chqrlie

@chqrlieforyellowblockquotes ใช่เพิ่งสังเกตเห็นเหมือนกันฉันแก้ไขด้วย ~ แทน แต่longจะทำงานได้ดีเท่า ๆ กัน
jpa

6

วิธีอื่น - การใช้union:

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

ในโปรแกรม:

...
B2I16 conv;

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

first_byteและsecond_byteสามารถเปลี่ยนได้ตามรุ่น endian น้อยหรือใหญ่ วิธีนี้ไม่ได้ดีกว่า แต่เป็นหนึ่งในทางเลือก


2
ชนิดของยูเนี่ยนไม่ได้ระบุพฤติกรรมที่ไม่ระบุอันน่าทึ่งใช่ไหม?
Maxim Egorushkin

1
@MaximEgorushkin: Wikipedia ไม่ใช่แหล่งข้อมูลที่เชื่อถือได้สำหรับการตีความมาตรฐาน C
Eric Postpischil

2
@EricPostpischil มุ่งเน้นไปที่ผู้ส่งสารมากกว่าข้อความจะไม่ฉลาด
Maxim Egorushkin

1
@ MaximEgorushkin: ใช่แล้วโอ้ฉันอ่านความคิดเห็นของคุณผิด สมมติว่าbyte[2]และint16_tมีขนาดเท่ากันมันเป็นหนึ่งหรือสองอย่างจาก orderings ที่เป็นไปได้ไม่ใช่ค่าสถานที่แบบบิตสับเปลี่ยนโดยพลการบางอย่าง ดังนั้นอย่างน้อยคุณก็สามารถตรวจสอบได้ในเวลารวบรวมว่าการดำเนินงานของ endianness มีอะไรบ้าง
Peter Cordes

1
มาตรฐานระบุไว้อย่างชัดเจนว่ามูลค่าของสมาชิกสหภาพเป็นผลมาจากการตีความบิตที่เก็บไว้ในสมาชิกเป็นการแสดงค่าของประเภทนั้น มีแง่มุมที่กำหนดการนำไปใช้งาน
MM

6

ตัวดำเนินการทางคณิตศาสตร์ เลื่อนและbitwise- หรือในนิพจน์(uint16_t)data[0] | ((uint16_t)data[1] << 8)ไม่ทำงานกับชนิดที่เล็กกว่าintดังนั้นuint16_tค่าเหล่านั้นจึงถูกเลื่อนระดับเป็นint(หรือunsignedถ้าsizeof(uint16_t) == sizeof(int)) แต่ถึงกระนั้นก็ควรให้คำตอบที่ถูกต้องเนื่องจากเพียง 2 ไบต์ที่ต่ำกว่ามีค่า

อีกเวอร์ชั่นที่ถูกต้องสำหรับการแปลงแบบบิ๊ก - เอนด์ถึงเอนด์ - เอนเดเนี่ยน

#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ใช้เพื่อคัดลอกการแทนค่าint16_tและเป็นวิธีที่เป็นไปตามมาตรฐานในการทำเช่นนั้น รุ่นนี้ยังรวบรวมลงใน 1 คำแนะนำmovbeให้ดูที่การชุมนุม


1
@MM เหตุผลหนึ่งที่__builtin_bswap16มีอยู่คือเนื่องจากการแลกเปลี่ยนไบต์ใน ISO C ไม่สามารถใช้งานได้อย่างมีประสิทธิภาพ
Maxim Egorushkin

1
ไม่จริง; คอมไพเลอร์สามารถตรวจจับได้ว่าโค้ดใช้การสลับไบต์และแปลเป็นบิวด์อินที่มีประสิทธิภาพ
MM

1
แปลงint16_tที่จะuint16_tมีการกำหนดไว้อย่างดี: ค่าลบแปลงค่ามากกว่าINT_MAXแต่การแปลงค่าเหล่านี้กลับไปuint16_tคือการดำเนินการกำหนดลักษณะการทำงาน: 6.3.1.3 การลงนามและจำนวนเต็มไม่ได้ลงนาม 1. เมื่อค่ากับชนิดจำนวนเต็มจะถูกแปลงเป็นจำนวนเต็มชนิดอื่น than_Bool อื่น ๆ ถ้า ค่าสามารถแสดงโดยชนิดใหม่มันไม่เปลี่ยนแปลง ... 3. มิฉะนั้นจะมีการลงชื่อประเภทใหม่และไม่สามารถแสดงค่าได้ ผลที่ได้คือการดำเนินการที่กำหนดไว้หรือสัญญาณที่กำหนดการดำเนินงานจะเพิ่มขึ้น
chqrlie

1
@MaximEgorushkin gcc ดูเหมือนว่าจะไม่สามารถทำได้ดีในรุ่น 16 บิต แต่เสียงดังกราวสร้างรหัสเดียวกันสำหรับntohs/ __builtin_bswapและ the |/ <<pattern: gcc.godbolt.org/z/rJ-j87
PSkocik

3
@MM: ฉันคิดว่า Maxim กำลังพูดว่า "ไม่สามารถใช้คอมไพเลอร์ปัจจุบันได้" แน่นอนว่าคอมไพเลอร์ไม่สามารถดูดหนึ่งครั้งและรับรู้การโหลดไบต์ที่ต่อเนื่องกันเป็นจำนวนเต็ม GCC7 หรือ 8 ได้แนะนำการรวมตัวกันโหลด / จัดเก็บใหม่สำหรับกรณีที่ไม่จำเป็นต้องใช้ไบต์ย้อนกลับหลังจาก GCC3 ลดลงเมื่อหลายสิบปีก่อน แต่โดยทั่วไปแล้วคอมไพเลอร์มีแนวโน้มที่จะต้องการความช่วยเหลือในทางปฏิบัติกับสิ่งต่างๆมากมายที่ซีพียูสามารถทำได้อย่างมีประสิทธิภาพ แต่ ISO C ที่ถูกทอดทิ้ง / ปฏิเสธที่จะเปิดเผยอย่างชัดเจน Portable ISO C ไม่ใช่ภาษาที่ดีสำหรับการเข้ารหัสบิต / ไบต์ที่มีประสิทธิภาพ
Peter Cordes

4

นี่เป็นอีกเวอร์ชั่นที่ต้องอาศัยการทำงานแบบพกพาและที่กำหนดไว้อย่างดี (ส่วนหัว #include <endian.h>ไม่ได้มาตรฐานรหัสคือ):

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

เวอร์ชั่นเล็ก ๆ ของ endian รวบรวมmovbeคำสั่งเดียวด้วยclang, gccเวอร์ชั่นนั้นน้อยที่สุด, ดูแอสเซมบลี .


@chqrlieforyellowblockquotes ความกังวลหลักของคุณดูเหมือนจะuint16_tเป็นการint16_tแปลงเวอร์ชันนี้ไม่มีการแปลงดังนั้นคุณจะไปที่นี่
Maxim Egorushkin

2

ฉันอยากจะขอบคุณผู้มีส่วนร่วมทั้งหมดสำหรับคำตอบของพวกเขา นี่คือสิ่งที่ผลงานโดยรวมกำลังเดือดลงไปที่:

  1. เป็นต่อ C มาตรฐาน7.20.1.1 ประเภทจำนวนเต็มแน่นอนความกว้าง : ประเภทuint8_t, int16_tและuint16_tต้องใช้ทั้งสองเป็นตัวแทนที่สมบูรณ์โดยไม่ต้องบิต padding ใด ๆ ดังนั้นบิตที่เกิดขึ้นจริงของการเป็นตัวแทนที่มีอย่างไม่น่าสงสัยเหล่านั้นของ 2 ไบต์ในอาร์เรย์ในการสั่งซื้อที่กำหนดโดย ชื่อฟังก์ชั่น
  2. คำนวณค่า 16 บิตที่ไม่ได้ลงนามด้วย (unsigned)data[0] | ((unsigned)data[1] << 8)การคอมไพล์ (สำหรับรุ่น endian น้อย) ไปยังคำสั่งเดียวและให้ค่า 16 บิตที่ไม่ได้ลงนาม
  3. ตาม C 6.3.1.3มาตรฐานจำนวนเต็มที่ลงนามและไม่ได้ลงนาม : การแปลงค่าชนิดเป็นประเภทuint16_tที่ลงนามint16_tมีพฤติกรรมการใช้งานที่กำหนดไว้หากค่าไม่ได้อยู่ในช่วงของประเภทปลายทาง ไม่มีการจัดเตรียมพิเศษสำหรับประเภทที่มีการกำหนดการแสดงอย่างแม่นยำ
  4. เพื่อหลีกเลี่ยงพฤติกรรมการดำเนินงานที่กำหนดไว้นี้หนึ่งสามารถทดสอบถ้าค่าไม่ได้ลงนามมีขนาดใหญ่กว่าและคำนวณค่าลงนามที่สอดคล้องกันโดยการลบINT_MAX 0x10000การทำเช่นนี้สำหรับค่าทั้งหมดตามที่แนะนำโดยzwolอาจสร้างค่าที่อยู่นอกช่วงที่int16_tมีพฤติกรรมการใช้งานที่กำหนดไว้เหมือนกัน
  5. การทดสอบ0x8000บิตจะทำให้คอมไพเลอร์สร้างโค้ดที่ไม่มีประสิทธิภาพ
  6. การแปลงที่มีประสิทธิภาพมากขึ้นโดยไม่ใช้พฤติกรรมที่กำหนดไว้ใช้ประเภทการติดตามผ่านทางสหภาพ แต่การอภิปรายเกี่ยวกับความชัดเจนของวิธีการนี้ยังคงเปิดอยู่แม้ในระดับคณะกรรมการของมาตรฐาน C
  7. ประเภทเล่นสำนวนสามารถดำเนินการ portably memcpyและมีพฤติกรรมที่กำหนดไว้โดยใช้

การรวมจุดที่ 2 และ 7 นี่คือโซลูชันแบบพกพาและกำหนดอย่างสมบูรณ์ที่รวบรวมได้อย่างมีประสิทธิภาพในคำสั่งเดียวด้วยgccและ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- บิต :

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

ฉันไม่ใช่นักกฎหมายด้านภาษา แต่มีเพียงcharนามแฝงเท่านั้นที่สามารถเป็นนามแฝงหรือมีการแสดงวัตถุประเภทอื่น uint16_tไม่ได้เป็นหนึ่งcharชนิดเพื่อให้memcpyของuint16_tที่จะint16_tเป็นพฤติกรรมที่ไม่ดีที่กำหนด มาตรฐานจำเป็นต้องมีchar[sizeof(T)] -> T > char[sizeof(T)]การแปลงด้วยmemcpyเท่านั้น
Maxim Egorushkin

memcpyการuint16_tที่จะint16_tมีการดำเนินการตามที่กำหนดไว้ที่ดีที่สุดไม่พกพาไม่ดีที่กำหนดตรงตามที่ได้รับมอบหมายจากที่หนึ่งไปยังที่อื่น ๆ memcpyและคุณไม่สามารถหลีกเลี่ยงได้อย่างน่าอัศจรรย์ว่า มันไม่สำคัญว่าจะuint16_tใช้การแสดงแทนสองหรือไม่หรือมีบิตแพ็ดดิ้งอยู่หรือไม่ - นั่นไม่ใช่พฤติกรรมที่กำหนดหรือกำหนดโดยมาตรฐาน C
Maxim Egorushkin

ด้วยคำพูดมากมายคำว่า "ทางออก" ของคุณนั้นเปลี่ยนr = uไปเป็นคำ ๆ หนึ่งmemcpy(&r, &u, sizeof u)แต่คำตอบไม่ดีไปกว่าคำว่าเดิม?
Maxim Egorushkin
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.