用于内存对齐的自定义数据大小

Custom data size for memory alignment

本文关键字:数据 自定义 内存 对齐 用于      更新时间:2023-10-16

每个数据类型都有一定的范围,具体取决于硬件。例如,在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;
};

这个想法是能够将每个变量都放入最小的比特大小,并跟踪剩余的比特数。

从理论上讲,可以利用每一个未使用的位,提高内存效率。显然,这永远不会像这样奏效,但这个例子只是为了解释这个概念。

这比您试图描述的要复杂得多,因为需要对对象和对象内的项进行对齐。例如,如果编译器决定一个整数项是structclass中的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%。

当然,就像许多不同的东西一样,这实际上取决于确切的情况,对象是如何使用的,缓存大小是多少,它到底是什么处理器,它周围其他地方有多少其他代码和数据也在使用缓存空间。唯一可以确定的方法是对您的代码进行实验。

[我运行了几次代码,虽然我并不总是得到相同的结果,但我总是得到相似的比例结果]