用于内存对齐的自定义数据大小
Custom data size for memory alignment
每个数据类型都有一定的范围,具体取决于硬件。例如,在32位机器上,int的范围为-2147483648到2147483647。
C++编译器"填充"对象内存以适应特定的大小。我敢肯定是2、4、8、16、32、64等等。这可能也取决于机器。
我想手动对齐我的对象以满足填充要求。有没有办法:
- 确定程序在哪台机器上运行
- 确定填充大小
- 根据位大小设置自定义数据类型
我以前在Java中使用过位集,但我不熟悉C++。至于机器需求,我知道不同硬件集的程序在C++中的编译方式通常不同,所以我想知道这是否可能。
示例->
/*getHardwarePack size obviously doesn't exist, just here to explain. What I'm trying to get
here would be the minimum alignment size for the machine the program is running on*/
#define PACK_SIZE = getHardwarePackSize();
#define MONTHS = 12;
class date{
private:
//Pseudo code that represents making a custom type
customType monthType = MONTH/PACK_SIZE;
monthType.remainder = MONTH % PACK_SIZE;
monthType months = 12;
};
这个想法是能够将每个变量都放入最小的比特大小,并跟踪剩余的比特数。
从理论上讲,可以利用每一个未使用的位,提高内存效率。显然,这永远不会像这样奏效,但这个例子只是为了解释这个概念。
这比您试图描述的要复杂得多,因为需要对对象和对象内的项进行对齐。例如,如果编译器决定一个整数项是struct
或class
中的16个字节,那么它很可能会决定"啊,我可以使用对齐的SSE指令来加载这个数据,因为它是在16个字节对齐的"(或者在ARM、PowerPC等中类似的东西)。因此,如果您至少不满足代码中的对齐,您将导致程序出错(崩溃或误读数据,具体取决于体系结构)。
通常,编译器使用和给出的对齐对于编译器所针对的任何体系结构都是"正确的"。更改它通常会导致性能变差。当然,并不总是这样,但在你摆弄它之前,你最好确切地知道自己在做什么。在之前/之后测量性能,并彻底测试是否没有任何损坏。
填充通常只是到下一个"最大类型的最小对齐"——例如,如果struct
只包含int
和几个char
变量,则它将被填充到4个字节[根据需要在结构内部和末尾]。对于double
,填充到8个字节是为了确保,但三个double
通常会占用8*3个字节,而没有进一步的填充。
此外,在编译期间比在运行时更好地确定您正在执行(或将在什么硬件上执行)。在运行时,您的代码已经生成,并且代码已经加载。在这一点上,你无法真正改变事物的偏移和对齐。
如果您使用的是gcc或clang编译器,则可以使用__attribute__((aligned(n)))
,例如int x[4] __attribute__((aligned(32)));
将创建一个与32字节对齐的16字节数组。这可以在结构或类内部完成,也可以用于您正在使用的任何变量。但这是一个编译时选项,不能在运行时使用。
在C++11以后的版本中,还可以找到类型或变量与alignof
的对齐。
注意,它给出了类型所需的对齐方式,所以如果你做了一些愚蠢的事情,比如:
int x;
char buf[4 * sizeof(int)];
int *p = (int *)buf + 7;
std::cout << alignof(*p) << std::endl;
代码将打印4,尽管buf+7
的对齐可能是3(7模4)。
无法在运行时选择类型。C++是一种静态类型的语言:某种东西的类型是在运行时确定的——当然,从基类派生的类可以在运行时创建,但对于任何给定的对象,它都有一个类型,永远都有,直到不再分配。
最好在编译时做出这样的选择,因为这会使编译器的代码更加直接,并且与在运行时做出选择相比,可以进行更好的优化,因为您必须在运行时决定使用某段代码的分支a或分支B。
作为对齐与未对齐访问的示例:
#include <cstdio>
#include <cstdlib>
#include <vector>
#define LOOP_COUNT 1000
unsigned long long rdtscl(void)
{
unsigned int lo, hi;
__asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
return ( (unsigned long long)lo)|( ((unsigned long long)hi)<<32 );
}
struct A
{
long a;
long b;
long d;
char c;
};
struct B
{
long a;
long b;
long d;
char c;
} __attribute__((packed));
std::vector<A> arr1(LOOP_COUNT);
std::vector<B> arr2(LOOP_COUNT);
int main()
{
for (int i = 0; i < LOOP_COUNT; i++)
{
arr1[i].a = arr2[i].a = rand();
arr1[i].b = arr2[i].b = rand();
arr1[i].c = arr2[i].c = rand();
arr1[i].d = arr2[i].d = rand();
}
printf("align A %zd, size %zdn", alignof(A), sizeof(A));
printf("align B %zd, size %zdn", alignof(B), sizeof(B));
for(int loops = 0; loops < 10; loops++)
{
printf("Run %dn", loops);
size_t sum = 0;
size_t sum2 = 0;
unsigned long long before = rdtscl();
for (int i = 0; i < LOOP_COUNT; i++)
sum += arr1[i].a + arr1[i].b + arr1[i].c + arr1[i].d;
unsigned long long after = rdtscl();
printf("ARR1 %lld sum=%zdn",(after - before), sum);
before = rdtscl();
for (int i = 0; i < LOOP_COUNT; i++)
sum2 += arr2[i].a + arr2[i].b + arr2[i].c + arr2[i].d;
after = rdtscl();
printf("ARR2 %lld sum=%zdn",(after - before), sum2);
}
}
[部分代码取自另一个项目,所以它可能不是有史以来最整洁的C++代码,但它让我从零开始编写代码,这与项目无关]
然后结果:
$ ./a.out
align A 8, size 32
align B 1, size 25
Run 0
ARR1 5091 sum=3218410893518
ARR2 5051 sum=3218410893518
Run 1
ARR1 3922 sum=3218410893518
ARR2 4258 sum=3218410893518
Run 2
ARR1 3898 sum=3218410893518
ARR2 4241 sum=3218410893518
Run 3
ARR1 3876 sum=3218410893518
ARR2 4184 sum=3218410893518
Run 4
ARR1 3875 sum=3218410893518
ARR2 4191 sum=3218410893518
Run 5
ARR1 3876 sum=3218410893518
ARR2 4186 sum=3218410893518
Run 6
ARR1 3875 sum=3218410893518
ARR2 4189 sum=3218410893518
Run 7
ARR1 3925 sum=3218410893518
ARR2 4229 sum=3218410893518
Run 8
ARR1 3884 sum=3218410893518
ARR2 4210 sum=3218410893518
Run 9
ARR1 3876 sum=3218410893518
ARR2 4186 sum=3218410893518
如您所见,使用arr1
对齐的代码大约需要3900个时钟周期,而使用arr2
对齐的代码则需要4200个时钟周期。因此,大约4000个循环中有300个循环,如果我的"薄荷醇算术"正确的话,大约有7.5%。
当然,就像许多不同的东西一样,这实际上取决于确切的情况,对象是如何使用的,缓存大小是多少,它到底是什么处理器,它周围其他地方有多少其他代码和数据也在使用缓存空间。唯一可以确定的方法是对您的代码进行实验。
[我运行了几次代码,虽然我并不总是得到相同的结果,但我总是得到相似的比例结果]
- 如何创建从Maya(或类似程序)到虚幻引擎的自定义数据导出插件
- std::ranges::elements_view,用于自定义类似元组的数据
- 通过 NIF 从C++返回自定义数据结构
- 我们可以在套接字编程中将自定义数据作为辅助数据发送吗?
- 自定义数据结构的优点是什么?
- 如何在 omnet++ 中发送自定义数据包?
- 错误 - 自定义数据类型作为有效负载,带有提升::几何
- C++哈希表 - 如何解决自定义数据类型作为键的unordered_map冲突?
- 如何在自定义 LLVM 传递之间正确传递数据结构
- 从自定义数据类型向量中删除重复元素
- C++:自定义数据类型向量错误的队列
- 如何使用自定义流操纵器在类实例中保存数据
- C++ - 按自定义数据类型向量的值删除元素
- 自定义堆栈上 std::string 数据的输出
- 将自定义 Java 数据模型传递给我的本机代码
- 如何使用自定义比较器初始化类数据成员,该成员是 std::set
- 通过套接字发送的自定义数据包
- 从自定义结构数组中提取数据
- 将 OpenGL VBO 与自定义类/数据结构一起使用
- QML自定义列表数据类型