Business
Jobs
  • About Us
  • Solutions
    • Job Postings
      Post your job and receive qualified candidates in 48h.
    • Candidate Assessments
      500+ technical and psychological tests, plus anti-fraud.
    • Headhunting
      Tailor-made executive search from start to finish.
    • Payroll + EOR
      Payroll dispersal and EOR across 15+ LATAM countries.
  • Pricing
  • Jobs

0

253
Views
¿Cuál es la forma correcta de convertir 2 bytes en un entero de 16 bits con signo?

En esta respuesta , zwol hizo esta afirmación:

La forma correcta de convertir dos bytes de datos de una fuente externa en un entero con signo de 16 bits es con funciones auxiliares como esta:

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

Cuál de las funciones anteriores es apropiada depende de si la matriz contiene una representación little endian o big endian. Endianness no es el problema en cuestión aquí, me pregunto por qué zwol resta 0x10000u del valor uint32_t convertido a int32_t .

¿Por qué es esta la forma correcta ?

¿Cómo evita el comportamiento definido por la implementación al convertir al tipo de devolución?

Dado que puede asumir la representación del complemento a 2, ¿cómo fallaría esta conversión más simple: return (uint16_t)val;

Lo que está mal con esta solución ingenua:

 int16_t le16_to_cpu_signed(const uint8_t data[static 2]) { return (uint16_t)data[0] | ((uint16_t)data[1] << 8); }
over 4 years ago · Santiago Trujillo
7 answers
Answer question

0

Los operadores aritméticos shift y bit a bit-or en expresión (uint16_t)data[0] | ((uint16_t)data[1] << 8) no funcionan en tipos más pequeños que int , por lo que esos valores de uint16_t se promocionan a int (o unsigned si sizeof(uint16_t) == sizeof(int) ). Sin embargo, eso debería dar la respuesta correcta, ya que solo los 2 bytes inferiores contienen el valor.

Otra versión pedantemente correcta para la conversión big-endian a little-endian (suponiendo una CPU little-endian) es:

 #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 se usa para copiar la representación de int16_t y esa es la forma estándar de hacerlo. Esta versión también se compila en 1 instrucción movbe , consulte ensamblado .

over 4 years ago · Santiago Trujillo Report

0

Otro método - usando union :

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

En programa:

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

first_byte y second_byte se pueden intercambiar de acuerdo con el modelo endian pequeño o grande. Este método no es mejor pero es una de las alternativas.

over 4 years ago · Santiago Trujillo Report

0

Si int es de 16 bits, su versión se basa en el comportamiento definido por la implementación si el valor de la expresión en la declaración de return está fuera del rango de int16_t .

Sin embargo, la primera versión también tiene un problema similar; por ejemplo, si int32_t es una definición de tipo para int y los bytes de entrada son ambos 0xFF , entonces el resultado de la resta en la declaración de devolución es UINT_MAX , lo que provoca un comportamiento definido por la implementación cuando se convierte a int16_t .

En mi humilde opinión, la respuesta a la que se vincula tiene varios problemas importantes.

over 4 years ago · Santiago Trujillo Report

0

Esto debería ser pedantemente correcto y funcionar también en plataformas que usan bits de signo o representaciones de complemento a 1 , en lugar del complemento a 2 habitual. Se supone que los bytes de entrada están en complemento a 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; }

Debido a la sucursal, será más caro que otras opciones.

Lo que esto logra es que evita cualquier suposición sobre cómo la representación int se relaciona con la representación unsigned en la plataforma. La conversión a int es necesaria para preservar el valor aritmético de cualquier número que se ajuste al tipo de destino. Debido a que la inversión asegura que el bit superior del número de 16 bits sea cero, el valor se ajustará. Entonces el - unario y la resta de 1 aplican la regla habitual para la negación del complemento a 2. Según la plataforma, INT16_MIN aún podría desbordarse si no encaja en el tipo int en el destino, en cuyo caso se debe usar long .

La diferencia con la versión original en la pregunta viene en el tiempo de devolución. Si bien el original siempre restaba 0x10000 y el complemento de 2 permitía que el desbordamiento firmado lo envolviera en el rango int16_t , esta versión tiene el if explícito que evita el reinicio firmado (que no está definido ).

Ahora, en la práctica, casi todas las plataformas en uso hoy en día usan la representación de complemento a 2. De hecho, si la plataforma tiene stdint.h compatible con el estándar que define int32_t , debe usar el complemento a 2 para ello. Donde este enfoque a veces resulta útil es con algunos lenguajes de secuencias de comandos que no tienen ningún tipo de datos enteros: puede modificar las operaciones que se muestran arriba para los flotantes y obtendrá el resultado correcto.

over 4 years ago · Santiago Trujillo Report

0

Aquí hay otra versión que se basa solo en comportamientos portátiles y bien definidos (el encabezado #include <endian.h> no es estándar, el código sí lo es):

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

La versión little-endian se compila en una sola instrucción movbe con clang , la versión gcc es menos óptima, consulte ensamblado .

over 4 years ago · Santiago Trujillo Report

0

Quiero agradecer a todos los colaboradores por sus respuestas. Esto es a lo que se reduce el trabajo colectivo:

  1. Según el Estándar C 7.20.1.1 Tipos de enteros de ancho exacto : los tipos uint8_t , int16_t y uint16_t deben usar la representación de complemento a dos sin bits de relleno, por lo que los bits reales de la representación son inequívocamente los de los 2 bytes en la matriz, en el orden especificado por los nombres de las funciones.
  2. calcular el valor de 16 bits sin firmar con (unsigned)data[0] | ((unsigned)data[1] << 8) (para la versión little endian) se compila en una sola instrucción y produce un valor de 16 bits sin signo.
  3. Según el estándar C 6.3.1.3 Enteros con y sin signo : convertir un valor de tipo uint16_t a tipo con int16_t tiene un comportamiento definido por la implementación si el valor no está en el rango del tipo de destino. No se prevén disposiciones especiales para los tipos cuya representación esté definida con precisión.
  4. para evitar este comportamiento definido por la implementación, se puede probar si el valor sin signo es mayor que INT_MAX y calcular el valor con signo correspondiente restando 0x10000 . Hacer esto para todos los valores sugeridos por zwol puede producir valores fuera del rango de int16_t con el mismo comportamiento definido por la implementación.
  5. probar el bit 0x8000 explícitamente hace que los compiladores produzcan código ineficiente.
  6. una conversión más eficiente sin un comportamiento definido de implementación utiliza juegos de palabras a través de una unión, pero el debate sobre la definición de este enfoque aún está abierto, incluso a nivel del Comité de Estándares C.
  7. El juego de palabras se puede realizar de forma portátil y con un comportamiento definido mediante memcpy .

Combinando los puntos 2 y 7, aquí hay una solución portátil y completamente definida que se compila de manera eficiente en una sola instrucción con gcc y 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; }

Asamblea de 64 bits :

 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
over 4 years ago · Santiago Trujillo Report

0

¿Por qué no simplemente usar su "solución ingenua", sino convertir cada elemento en int16_t en lugar de uint16_t ?

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

Entonces no tendría que lidiar con la conversión de entradas sin firmar a entradas con firma (y posiblemente estar fuera del rango de entradas con firma).

over 4 years ago · Santiago Trujillo Report
Answer question
Find remote jobs

Discover the new way to find a job!

Top jobs
Top job categories
Business
Post vacancy Pricing Sales
Legal
Terms and conditions Privacy policy
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Show me some job opportunities
There's an error!