Чтобы избежать копирования больших объемов данных, желательно mmap
двоичный файл и непосредственная обработка необработанных данных.Этот подход имеет несколько преимуществ, в том числе перевод пейджинга в операционную систему.К сожалению, я понимаю, что очевидная реализация приводит к неопределенному поведению (UB).
Мой пример использования следующий: создайте двоичный файл, содержащий некоторый заголовок, идентифицирующий формат и предоставляющий метаданные (в данном случае простоколичество double
значений).Оставшаяся часть файла содержит необработанные двоичные значения, которые я хочу обработать без необходимости сначала копировать файл в локальный буфер (поэтому я в первую очередь сопоставляю файл с памятью).Приведенная ниже программа является полным (если простым) примером (я считаю, что все места, отмеченные как UB[X]
, ведут к UB):
// C++ Standard Library
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <fstream>
#include <iostream>
#include <numeric>
// POSIX Library (for mmap)
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
constexpr char MAGIC[8] = {"1234567"};
struct Header {
char magic[sizeof(MAGIC)] = {'\0'};
std::uint64_t size = {0};
};
static_assert(sizeof(Header) == 16, "Header size should be 16 bytes");
static_assert(alignof(Header) == 8, "Header alignment should be 8 bytes");
void write_binary_data(const char* filename) {
Header header;
std::copy_n(MAGIC, sizeof(MAGIC), header.magic);
header.size = 100u;
std::ofstream fp(filename, std::ios::out | std::ios::binary);
fp.write(reinterpret_cast<const char*>(&header), sizeof(Header));
for (auto k = 0u; k < header.size; ++k) {
double value = static_cast<double>(k);
fp.write(reinterpret_cast<const char*>(&value), sizeof(double));
}
}
double read_binary_data(const char* filename) {
// POSIX mmap API
auto fp = ::open(filename, O_RDONLY);
struct stat sb;
::fstat(fp, &sb);
auto data = static_cast<char*>(
::mmap(nullptr, sb.st_size, PROT_READ, MAP_PRIVATE, fp, 0));
::close(fp);
// end of POSIX mmap API (all error handling ommitted)
// UB1
const auto header = reinterpret_cast<const Header*>(data);
// UB2
if (!std::equal(MAGIC, MAGIC + sizeof(MAGIC), header->magic)) {
throw std::runtime_error("Magic word mismatch");
}
// UB3
auto beg = reinterpret_cast<const double*>(data + sizeof(Header));
// UB4
auto end = std::next(beg, header->size);
// UB5
auto sum = std::accumulate(beg, end, double{0});
::munmap(data, sb.st_size);
return sum;
}
int main() {
const double expected = 4950.0;
write_binary_data("test-data.bin");
if (auto sum = read_binary_data("test-data.bin"); sum == expected) {
std::cout << "as expected, sum is: " << sum << "\n";
} else {
std::cout << "error\n";
}
}
Скомпилируйте и запустите как:
$ clang++ example.cpp -std=c++17 -Wall -Wextra -O3 -march=native
$ ./a.out
$ as expected, sum is: 4950
В реальной жизни фактический двоичный формат намного сложнее, но сохраняет те же свойства: основные типы хранятся в двоичном файле с правильным выравниванием.
Мой вопрос: как вы справляетесь с этим вариантом использования?
Я нашел много ответов, которые я считаю противоречивыми.
Некоторые ответы недвусмысленно утверждают, что объекты следует строить локально.Это вполне может иметь место, но серьезно усложняет любые операции с массивами.
Комментарии в других местах , кажется, согласны с UB-природой этой конструкции, но есть некоторые разногласия.
Формулировка cppreference , по крайней мере для меня, сбивает с толку.Я бы интерпретировал это как «то, что я делаю, совершенно законно».В частности, этот абзац:
Всякий раз, когда делается попытка прочитать или изменить сохраненное значение объекта типа DynamicType с помощью glvalue типа AliasedType, поведение не определено, если не выполняется одно из следующих условий:
- AliasedType и DynamicType похожи.
- AliasedType является (возможно, cv-квалифицированным) подписанным или неподписанным вариантом DynamicType.
- AliasedType является std :: byte, (начиная с C ++ 17) char или unsigned char: это разрешаетпроверка представления объекта любого объекта в виде массива байтов.
Возможно, C ++ 17 дает некоторую надежду с std::launder
или что яПридется подождать до C ++ 20 чего-то вроде std::bit_cast
.
А пока как вы решите эту проблему?
Ссылка на онлайн-демонстрацию: https://onlinegdb.com/rk_xnlRUV
Упрощенный пример на C
Насколько я понимаю, правильно, что следующая программа на C не демонстрирует Undefined Behavior?Я понимаю, что приведение указателя через буфер char
не участвует в строгих правилах алиасинга.
#include <stdint.h>
#include <stdio.h>
struct Header {
char magic[8];
uint64_t size;
};
static void process(const char* buffer) {
const struct Header* h = (const struct Header*)(buffer);
printf("reading %llu values from buffer\n", h->size);
}
int main(int argc, char* argv[]) {
if (argc != 2) {
return 1;
}
// In practice, I'd pass the buffer through mmap
FILE* fp = fopen(argv[1], "rb");
char buffer[sizeof(struct Header)];
fread(buffer, sizeof(struct Header), 1, fp);
fclose(fp);
process(buffer);
}
Я могу скомпилировать и запустить этот код C, передав файл, созданный оригинальной программой C ++ иработает как положено:
$ clang struct.c -std=c11 -Wall -Wextra -O3 -march=native
$ ./a.out test-data.bin
reading 100 values from buffer