我正在处理一些我想要内存映射一些包含数字数据的大文件的东西。问题是数据可以是多种格式,包括实数字节/短/ int /长/浮点/双和复数字节/短/ int /长/浮点/双。自然地处理所有这些类型很快就会变得笨拙,所以我在考虑实现一个可以为用户进行实时类型转换的内存映射接口。
我非常喜欢映射文件的想法,这样你就可以在内存中找到一个指针,做你需要的任何事情,然后取消映射。没有缓冲区或任何其他需要。因此,一个读取数据并为我进行类型转换的函数会花费很多时间。
我以为我可以将内存映射到正在操作的文件,然后同时映射匿名文件,以某种方式捕获页面提取/存储并按需进行类型转换。我将在64位工作,所以在这些情况下这会给你一个63位的地址空间,但是很好。
有没有人知道这种mmap挂钩是否可能,如果是这样,它将如何实现?
答案 0 :(得分:1)
阅读部分对我来说听起来有些可行。我没有这方面的经验,但原则上有一个信号处理程序获取您的数据并在您访问用户呈现的缓冲区中尚未出现的页面时立即进行翻译。但是,这样的事情可能效率很低,每个页面都有一个上下文切换。
另一方面,我猜想会更难。每个默认写入都是异步的,因此很难捕获它们。
所以你想要的“一半”是可能的:总是以用户想要的格式在新文件中写入数据,但在阅读这样的文件时会动态地自动翻译。
但是,对于您来说,更重要的是,您对不同的存储表示有明确的语义,并且您正确地封装了对数据项的读取或写入。如果您有这样的界面(类似于E
位于i
的类型T
“),您可以轻松触发与目标格式相关的转换。
答案 1 :(得分:1)
是(-ish)。您可以创建无法访问的mmap
区域。每当有人试图触摸一个时,通过修复其权限,填写它并恢复来处理SIGSEGV
。
long *long_view =
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
double *double_view =
mmap(NULL, 4096, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
static void on_segv(int signum, siginfo_t *info, void *data) {
void *addr = info->si_addr;
if ((uintptr_t)addr - (uintptr_t)long_view < 4096) {
mprotect(long_view, 4096, PROT_READ|PROT_WRITE);
/* translate from double_view to long_view */
mprotect(double_view, 4096, PROT_NONE);
} else if ((uintptr_t)addr - (uintptr_t)double_view < 4096) {
mprotect(double_view, 4096, PROT_READ|PROT_WRITE);
/* translate from long_view to long_view */
mprotect(double_view, 4096, PROT_NONE);
} else {
abort();
}
}
struct sigaction segv_action = {
.sa_sigaction = on_segv,
.sa_flags = SA_RESTART | SA_SIGINFO,
};
sigaction(SIGSEGV, &segv_action, NULL);
long_view[0] = 42;
/* hopefully, this will trigger the code to fixup double_view and resume */
printf("%g\n", double_view[0]);
(未经测试,但这些方面应该有用......)
如果您不想一次填满整个页面,我认为这仍然可行......第三个参数可以转换为ucontext_t *
,您可以使用它来解码正在执行的指令并修复好像它已经执行了预期的操作,同时让记忆PROT_NONE
进一步访问...但是由于你捕获每个访问而不仅仅是第一个访问,它会慢得多。
答案 2 :(得分:1)
你有没有使用访问器功能的原因?
有两种基本情况:结构化数据和普通数据。结构化数据具有混合数据类型,纯数据只有您列出的数据格式。如果您可以为每个使用的类型存储原型值(具有不同的字节组件),您也可以支持透明字节序更正(对于您列出的所有类型,总共28或30个字节 - 复杂类型只是对,并且具有相同的字节顺序作为基本组件)。我已经使用这种方法来存储和访问来自分子动力学模拟的原子数据,并且在实践中非常有效 - 我测试过的最快的便携式数据。
我会用一个结构来描述“文件”(后备文件,内存映射,字节顺序和数据格式,如果是普通数据):
struct file {
int descriptor;
size_t size;
unsigned char *data;
unsigned int endian; /* Relative to current architecture */
unsigned int format; /* endian | format, for unstructured files */
};
#define ENDIAN_I16_MASK 0x0001
#define ENDIAN_I16_12 0x0000
#define ENDIAN_I16_21 0x0001
#define ENDIAN_I32_MASK 0x0006
#define ENDIAN_I32_1234 0x0000
#define ENDIAN_I32_4321 0x0002
#define ENDIAN_I32_2143 0x0004
#define ENDIAN_I32_3412 0x0006
#define ENDIAN_I64_MASK 0x0018
#define ENDIAN_I64_1234 0x0000
#define ENDIAN_I64_4321 0x0008
#define ENDIAN_I64_2143 0x0010
#define ENDIAN_I64_3412 0x0018
#define ENDIAN_F16_MASK 0x0020
#define ENDIAN_F16_12 0x0000
#define ENDIAN_F16_21 0x0020
#define ENDIAN_F32_MASK 0x00C0
#define ENDIAN_F32_1234 0x0000
#define ENDIAN_F32_4321 0x0040
#define ENDIAN_F32_2143 0x0080
#define ENDIAN_F32_3412 0x00C0
#define ENDIAN_F64_MASK 0x0300
#define ENDIAN_F64_1234 0x0000
#define ENDIAN_F64_4321 0x0100
#define ENDIAN_F64_2143 0x0200
#define ENDIAN_F64_3412 0x0300
#define FORMAT_MASK 0xF000
#define FORMAT_I8 0x1000
#define FORMAT_I16 0x2000
#define FORMAT_I32 0x3000
#define FORMAT_I64 0x4000
#define FORMAT_P8 0x5000 /* I8 pair ("complex I8") */
#define FORMAT_P16 0x6000 /* I16 pair ("complex I16") */
#define FORMAT_P32 0x7000 /* I32 pair ("complex I32") */
#define FORMAT_P64 0x8000 /* I64 pair ("complex I64") */
#define FORMAT_R16 0x9000 /* BINARY16 IEEE-754 floating-point */
#define FORMAT_R32 0xA000 /* BINARY32 IEEE-754 floating-point */
#define FORMAT_R64 0xB000 /* BINARY64 IEEE-754 floating-point */
#define FORMAT_C16 0xC000 /* BINARY16 IEEE-754 complex */
#define FORMAT_C32 0xD000 /* BINARY32 IEEE-754 complex */
#define FORMAT_C64 0xE000 /* BINARY64 IEEE-754 complex */
访问者功能可以以各种方式实现。在Linux中,标记为static inline
的函数也与宏一样快。
由于double
类型没有完全覆盖64位整数类型(因为它在尾数中只有52位),所以我定义了一个数字结构,
#include <stdint.h>
struct number {
int64_t ireal;
int64_t iimag;
double freal;
double fimag;
};
并且访问者函数始终填写四个字段。使用GCC,您还可以创建一个宏来使用自动类型检测来定义结构编号:
#define Number(x) \
( __builtin_types_compatible_p(__typeof__ (x), double) ? number_double(x) : \
__builtin_types_compatible_p(__typeof__ (x), _Complex double) ? number_complex_double(x) : \
__builtin_types_compatible_p(__typeof__ (x), _Complex long) ? number_complex_long(x) : \
number_int64(x) )
static inline struct number number_int64(const int64_t x)
{
return (struct number){ .ireal = (int64_t)x,
.iimag = 0,
.freal = (double)x,
.fimag = 0.0 };
}
static inline struct number number_double(const double x)
{
return (struct number){ .ireal = (int64_t)x,
.iimag = 0,
.freal = x,
.fimag = 0.0 };
}
static inline struct number number_complex_long(const _Complex long x)
{
return (struct number){ .ireal = (int64_t)(__real__ (x)),
.iimag = (int64_t)(__imag__ (x)),
.freal = (double)(__real__ (x)),
.fimag = (double)(__imag__ (x)) };
}
static inline struct number number_complex_double(const _Complex double x)
{
return (struct number){ .ireal = (int64_t)(__real__ (x)),
.iimag = (int64_t)(__imag__ (x)),
.freal = __real__ (x),
.fimag = __imag__ (x) };
}
这意味着只要Number(value)
是整数或浮点实数或复数类型,struct number
就会构造一个正确的value
。
注意类型转换允许的整数和浮点组件如何设置为相同的值。 (对于幅度非常大的整数,浮点值将是近似值。在设置整数分量时,您还可以使用(int64_t)round(...)
来舍入而不是截断浮点参数。
您需要四个访问者功能:两个用于结构化数据,两个用于非结构化数据。对于非结构化(普通)数据:
static inline struct number file_get_number(const struct file *const file,
const size_t offset)
{
...
}
static inline void file_set_number(const struct file *const file,
const size_t offset,
const struct number number)
{
...
}
请注意,上面的offset
不是字节偏移量,而是数字的索引。对于结构化文件,您需要使用字节偏移量,并添加一个参数,指定文件中使用的数字格式:
static inline struct number file_get(const struct file *const file,
const size_t byteoffset,
const unsigned int format)
{
...
}
static inline void file_set(const struct file *const file,
const size_t byteoffset,
const unsigned int format,
const struct number number)
{
...
}
我省略的函数体(...)中所需的转换非常简单。您也可以通过一些技巧进行优化。例如,我喜欢调整字节顺序常量,以便低位始终是字节交换(ab - > ba,abcd - &gt; badc,abcdefgh - &gt; badcfehg),高位是短交换(abcd) - &gt; cdab,abcdefgh - &gt; cdabghef)。如果你想完全确定,你可能需要64位值的第三位(abcdefgh - &gt; efghabcd)。
函数体中的if或case语句确实会导致较小的访问开销,但它应该足够小,在实践中可以忽略。所有避免它的方法都会导致更复杂的代码。 (为了获得最大吞吐量,您需要对所有访问变体进行开放式编码,并在函数或宏中使用__builtin_types_compatible_p()
来确定要使用的正确变量。如果考虑字节顺序转换,则意味着相当多的功能我相信非常小的访问开销 - 每次访问最多几个时钟 - 更为可取。(我所有的测试都是I / O绑定,即使是200 Mb / s,所以对我来说,开销是完全的不相关的。)
通常,对于使用原型值的自动字节序转换,您只需测试该类型的每个可能的转换。只要原型值的每个字节组件都是唯一的,那么只有一个转换将产生正确的期望值。在某些体系结构中,整数和浮点值具有不同的字节序;这就是ENDIAN_
常量分别针对每种类型和大小的原因。
假设您已经实现了上述所有内容,那么在您的应用程序代码中,数据访问将类似于
struct file f;
/* Set first number to zero. */
file_set_number(&f, 0, Number(0));
/* Set second number according to variable v,
* which can be just about any numeric type. */
file_set_number(&f, 1, Number(v));
我希望你觉得这很有用。