用于内存对齐的自定义数据大小
Custom data size for memory alignment
每种数据类型都有一定的范围,具体取决于硬件。例如,在 32 位机器上,int 的范围是 -2147483648 到 2147483647。
C++ 编译器 'pad' 适合特定大小的对象内存。我很确定它是 2、4、8、16、32、64 等。这也可能取决于机器。
我想手动对齐我的对象以满足填充要求。有没有办法:
- 确定程序是什么机器运行在
- 确定填充大小
- 根据位大小设置自定义数据类型
我以前在 Java 中使用过 bitsets,但我不熟悉 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 个字节,它很可能会决定 "ah, I can use an aligned SSE instruction to load this data, because it is aligned at 16 bytes"(或 ARM、PowerPC 等中的类似内容)。因此,如果您至少不满足代码中的对齐方式,则会导致程序出错(崩溃或误读数据,具体取决于体系结构)。
通常,对于编译器所针对的任何体系结构,编译器使用和给出的对齐方式都是 "right"。更改它通常会导致更差的性能。当然,并非总是如此,但在使用它之前,您最好确切地知道自己在做什么。fiddle。并测量性能 before/after,并彻底测试没有任何问题。
填充通常只是到下一个 "minimum alignment for the largest type" - 例如如果 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++ 是一种静态类型语言:某些东西的类型是在运行时确定的——当然,从基类派生的 类 可以在运行时创建,但对于任何给定的对象,它只有一种类型,永远永远直到它不再分配。
最好在编译时做出这样的选择,因为它使编译器的代码更加直接,并且比在运行时做出选择更能优化,因为你必须做出运行时决定使用某段代码的 b运行ch A 或 b运行ch 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 %zd\n", alignof(A), sizeof(A));
printf("align B %zd, size %zd\n", alignof(B), sizeof(B));
for(int loops = 0; loops < 10; loops++)
{
printf("Run %d\n", 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=%zd\n",(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=%zd\n",(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 个周期,如果我的 "menthol arithmetic" 正常工作,大约 7.5%。
当然,就像很多不同的东西一样,这真的取决于具体情况,对象是如何使用的,缓存大小是多少,具体是什么处理器,其他地方有多少其他代码和数据围绕它也使用缓存-space。唯一可以确定的方法是试验您的代码。
[我 运行 代码多次,虽然我并不总是得到相同的结果,但我总是得到相似的比例结果]
每种数据类型都有一定的范围,具体取决于硬件。例如,在 32 位机器上,int 的范围是 -2147483648 到 2147483647。
C++ 编译器 'pad' 适合特定大小的对象内存。我很确定它是 2、4、8、16、32、64 等。这也可能取决于机器。
我想手动对齐我的对象以满足填充要求。有没有办法:
- 确定程序是什么机器运行在
- 确定填充大小
- 根据位大小设置自定义数据类型
我以前在 Java 中使用过 bitsets,但我不熟悉 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 个字节,它很可能会决定 "ah, I can use an aligned SSE instruction to load this data, because it is aligned at 16 bytes"(或 ARM、PowerPC 等中的类似内容)。因此,如果您至少不满足代码中的对齐方式,则会导致程序出错(崩溃或误读数据,具体取决于体系结构)。
通常,对于编译器所针对的任何体系结构,编译器使用和给出的对齐方式都是 "right"。更改它通常会导致更差的性能。当然,并非总是如此,但在使用它之前,您最好确切地知道自己在做什么。fiddle。并测量性能 before/after,并彻底测试没有任何问题。
填充通常只是到下一个 "minimum alignment for the largest type" - 例如如果 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++ 是一种静态类型语言:某些东西的类型是在运行时确定的——当然,从基类派生的 类 可以在运行时创建,但对于任何给定的对象,它只有一种类型,永远永远直到它不再分配。
最好在编译时做出这样的选择,因为它使编译器的代码更加直接,并且比在运行时做出选择更能优化,因为你必须做出运行时决定使用某段代码的 b运行ch A 或 b运行ch 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 %zd\n", alignof(A), sizeof(A));
printf("align B %zd, size %zd\n", alignof(B), sizeof(B));
for(int loops = 0; loops < 10; loops++)
{
printf("Run %d\n", 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=%zd\n",(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=%zd\n",(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 个周期,如果我的 "menthol arithmetic" 正常工作,大约 7.5%。
当然,就像很多不同的东西一样,这真的取决于具体情况,对象是如何使用的,缓存大小是多少,具体是什么处理器,其他地方有多少其他代码和数据围绕它也使用缓存-space。唯一可以确定的方法是试验您的代码。
[我 运行 代码多次,虽然我并不总是得到相同的结果,但我总是得到相似的比例结果]