Скопировать два элемента из массива uint8_t в переменную uint16_t с помощью указателей

#arrays #c #pointers #casting #endianness

#массивы #c #указатели #Кастинг #порядковый номер

Вопрос:

У меня есть uint8_t массив. Иногда мне нужно обработать два последовательных элемента как uint16_t , и скопировать их в другую uint16_t переменную. Элементы могут не обязательно начинаться с uint16_t границы слова.

В настоящее время я использую memcpy для этого:

 uint8_t bytes[] = { 0x1A, 0x2B, 0x3C, 0x4D };
uint16_t word = 0;
memcpy(amp;word, amp;bytes[1], sizeof(word));
 

gdb показывает, что это работает так, как ожидалось:

 (gdb) x/2bx amp;word
0x7fffffffe2e2: 0x2b    0x3c
 

Приведение ссылки на элемент массива к uint16_t тому, что будет скопирован только элемент массива, на который есть прямая ссылка:

 word = (uint16_t)bytes[1];

(gdb) x/2bx amp;word
0x7fffffffe2e2: 0x2b    0x00
 

Использование uint16_t указателя и указание его на адрес элемента массива, приведенного к uint16_t * результатам, приводит к тому, что на два последовательных элемента ссылаются, но не копируются:

 uint16_t *wordp = (uint16_t *)amp;bytes[1];

(gdb) x/2bx wordp
0x7fffffffe2e5: 0x2b    0x3c

bytes[1] = 0x5E;

(gdb) x/2bx wordp
0x7fffffffe2e5: 0x5e    0x3c
 

Присвоение обычной uint16_t переменной значения, разыменованного из uint16_t указателя, копирует оба последовательных элемента массива:

 word = *wordp;
bytes[1] = 0x6F;

(gdb) x/2bx wordp
0x7fffffffe2e5: 0x6f    0x3c
(gdb) x/2bx amp;word
0x7fffffffe2e2: 0x5e    0x3c
 

gcc не выдает никаких предупреждений при использовании -Wall опции.

Как я могу добиться того же результата без промежуточного указателя?

Есть ли какие-либо проблемы с использованием указателя, как описано выше, или с попыткой сделать это без промежуточного указателя?

Будет ли использование memcpy считаться предпочтительным при определенных сценариях?

Конечным использованием этого является обработка большого конечного байтового потока, поэтому я использую byteorder(3) функции по мере необходимости.

Комментарии:

1. Есть ли какая-либо причина, по которой вы не хотите использовать memcpy версию? Это самый безопасный способ избежать неопределенного поведения , и я бы ожидал, что любой приличный оптимизатор оптимизирует копии, если это возможно

2. memcpy считается предпочтительным из-за строгого правила псевдонимов. Другой вариант — сдвинуть и ИЛИ создать uint16_t . Это также устраняет проблемы с порядковым номером.

3. Нет особой причины не хотеть использовать memcpy ; это то, что я использую в 99% случаев. Первоначально я использовал shift и OR, но заменил их на memcpy . Затем я заметил связанное использование memcpy вызовов функций между цепочками, и возникли сомнения. Я думаю, что я знаю о вариантах, я просто не чувствую, что у меня достаточно опыта, чтобы сравнить их и оценить, что я должен использовать и почему.

4. Стоит отметить, что оптимизаторы компилятора, которые видят константу 2 как размер для memcpy вызова, почти всегда будут переписывать, чтобы вообще не включать вызов функции или цикл, и просто манипулировать байтами с помощью регистров. Тогда стоимость этого крайне незначительна, не беспокойтесь о накладных расходах функций или ненужном доступе к памяти за пределами кэшей.

Ответ №1:

 uint8_t bytes[] = { 0x1A, 0x2B, 0x3C, 0x4D };
uint16_t word = 0;
memcpy(amp;word, amp;bytes[1], sizeof(word));
 

Вышесказанное хорошо. Копирует байты напрямую. Если вы с удовольствием копируете байты по порядку и не беспокоитесь о порядке следования, это способ сделать это.

См. Ответ P__J supports women в Польше для доказательства того, что компиляторы достаточно умны, чтобы оптимизировать это использование, поэтому вам не нужно беспокоиться об оптимизации самостоятельно.

 word = (uint16_t)bytes[1];
 

Приведенное выше просто обнуляет ваше 8-битное значение до 16-битного значения (0x002B вместо 0x2B); предположительно, это не то, что вы хотите.

 uint16_t *wordp = (uint16_t *)amp;bytes[1];
word = *wordp;
 

Не делайте вышеописанного. Это приводит к неопределенному поведению. A uint16_t имеет более строгие требования к выравниванию, чем a uint8_t , и вы можете получить недопустимый адрес для типа указателя. То есть, будучи двухбайтовым типом данных, он требует адресов, которые существуют на границах, кратных двум (например, 0x2, 0x4, 0x6 и т.д.), Хотя uint8_t не имеет такого ограничения и может существовать в 0x1, 0x2, 0x3 и т.д.. (Подробнее о выравнивании см. Раздел 6.2.8 рабочего проекта C11)

При разыменовании вы также нарушили строгое правило псевдонимов, разыменовав объект с несовместимым типом. (Подробнее о совместимых типах см. Раздел 6.2.7 рабочего проекта C11)

Комментарии:

1. Спасибо, Кристиан. В ответ на ваше утверждение о порядке следования. Это зависит от того, что я делаю с данными. Для ситуаций, когда меня волнует порядковый номер, который я использую ntohs() для преобразования байтов после их копирования в uint16_t переменную.

2. Не могли бы вы также подробнее рассказать о строгих требованиях к выравниванию и о том, как я мог получить неверный адрес? Есть ли какой-нибудь способ заставить gcc предупредить меня? Или приведение меня твердо говорит, что я рад выстрелить себе в ногу?

3. Обновлено, добавлено немного больше о выравнивании и строгом сглаживании.

Ответ №2:

memcpy — самый безопасный способ. И при использовании современных компиляторов наиболее эффективный as memcpy вызываться не будет.

Пример (изменчивый для оптимизации orevent):

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

int main()
{
    volatile uint8_t bytes[4];
    volatile uint16_t word = 0;
    memcpy(amp;word, amp;bytes[1], sizeof(word));
}
 

и memcpy преобразуется в

         movzx   eax, WORD PTR [rsp-3]
        mov     WORD PTR [rsp-6], ax
 

https://godbolt.org/z/57n879