Каков наилучший подход при работе с структурами данных на диске

Я хотел бы знать, как лучше работать с структурами данных на диске, учитывая, что макет хранилища должен точно соответствовать логическому дизайну. Я считаю, что выравнивание и упаковка структуры не очень помогают, когда вам нужно иметь определенный макет для вашего хранилища.

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

например:

typedef struct __attribute__((packed, aligned(1))) foo {
   uint64_t some_stuff;
   uint8_t flag;
} foo;

Если я сохраню foo on-disk, значение "flag" появится в самом конце данных. Учитывая, что я могу легко использовать foo при чтении данных с использованием fread по типу & foo, тогда используя структуру обычно без каких-либо дополнительных байтов.

Вместо этого я предпочитаю делать это

#define foo_width sizeof(uint64_t)+sizeof(uint8_t)

uint8_t *foo = calloc(1, foo_width);

foo[0] = flag_value;
memcpy(foo+1, encode_int64(some_value), sizeof(uint64_t));

Затем я просто использую fwrite и fread для фиксации и чтения байтов, но позже распакую их, чтобы использовать данные, хранящиеся в разных логических полях.

Интересно, какой подход лучше всего использовать, учитывая, что расположение хранилища на диске соответствует логическому расположению... это был всего лишь пример...

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

Спасибо.

Ответ 1

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

Ответ 2

Я предпочитаю что-то близкое к вашему второму подходу, но без memcpy:

void store_i64le(void *dest, uint64_t value)
{  // Generic version which will work with any platform
  uint8_t *d = dest;
  d[0] = (uint8_t)(value);
  d[1] = (uint8_t)(value >> 8);
  d[2] = (uint8_t)(value >> 16);
  d[3] = (uint8_t)(value >> 24);
  d[4] = (uint8_t)(value >> 32);
  d[5] = (uint8_t)(value >> 40);
  d[6] = (uint8_t)(value >> 48);
  d[7] = (uint8_t)(value >> 56);
}

store_i64le(foo+1, some_value);

В типичном ARM вышеупомянутый метод store_i64le переводится в примерно 30 байтов - разумное компромисс между временем, пространством и сложностью. Не совсем оптимальный с точки зрения скорости, но не намного хуже, чем оптимальный с космической точки зрения, на чем-то вроде Cortex-M0, который не поддерживает неглавные записи. Обратите внимание, что написанный код имеет нулевую зависимость от порядкового байта машины. Если бы кто-то знал, что кто-то использует платформу little-endian, аппаратное обеспечение которой преобразует неглавные 32-битные обращения к последовательности 8- и 16-разрядных обращений, можно переписать метод как

void store_i64le(void *dest, uint64_t value)
{  // For an x86 or little-endian ARM which can handle unaligned 32-bit loads and stores
  uint32_t *d = dest;
  d[0] = (uint32_t)(value);
  d[1] = (uint32_t)(value >> 32);
}

который будет быстрее на платформах, где он будет работать. Обратите внимание, что метод будет вызываться так же, как и побайтовая версия; вызывающему абоненту не придется беспокоиться о том, какой подход использовать.

Ответ 3

Если вы находитесь в Linux или Windows, тогда просто скопируйте карту в файл и нарисуйте указатель на тип структуры C. Все, что вы пишете в этой отображаемой области, будет автоматически сброшено на диск самым эффективным способом, доступным для ОС. Это будет намного эффективнее, чем называть "писать" и минимальные проблемы для вас.

Как уже упоминалось, он не очень портативен. Чтобы быть переносимым между мини-эндиан и big-endian, общая стратегия состоит в том, чтобы написать весь файл в формате big-endian или little-endian и преобразовать при его доступе. Однако это отбрасывает вашу скорость. Способ сохранить вашу скорость - написать внешнюю утилиту, которая преобразует весь файл один раз, а затем запустить эту утилиту всякий раз, когда вы перемещаете структуру с одной платформы на другую.

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