首页 > 编程语言 >C++ 结构体对齐

C++ 结构体对齐

时间:2023-04-20 21:13:22浏览次数:46  
标签:std layout 字节 C++ 字段 对齐 size 结构

C++ 结构体对齐

引言

数据结构对齐是数据在计算机内存中排列和访问的方式。它由三个独立但相关的问题组成:数据对齐、数据结构填充和打包。现代计算机硬件中的 CPU 在数据自然对齐时最有效地执行内存读取和写入,这通常意味着数据的内存地址是数据大小的倍数。例如,在 32 位架构中,如果数据存储在四个连续字节中并且第一个字节位于4字节边界上,则数据可能是对齐的。

数据对齐是根据元素的自然对齐方式对齐元素。为了确保自然对齐,可能需要在结构元素之间或结构的最后一个元素之后插入一些填充。例如,在 32 位机器上,包含一个 16 位值后跟一个 32 位值的数据结构可以在 16 位值和 32 位值之间有 16 位填充以对齐 32 位值32 位边界上的值。或者,可以打包结构,省略填充,这可能会导致访问速度变慢,但会使用四分之三的内存。

尽管数据结构对齐是所有现代计算机的基本问题,但许多计算机语言和计算机语言实现会自动处理数据对齐。 Fortran、Ada、PL/I、Pascal、某些 C 和 C++ 实现、D、Rust、C#、和汇编语言至少允许部分控制数据结构填充,这在某些特殊情况下可能很有用。


如何计算填充

这里以64位操作系统为例,首先我们列出一些基本类型的对其大小。

类型 对齐大小
char(1byte) 1
short(2bytes) 2
long(4bytes) 4
float(4bytes) 4
double(8bytes) 8
long double(8bytes) 8

以long类型为例,long的对齐大小为4,代表着这个类型的地址应该可以被4所整除,也就是其二进制地址应该是以0b100结尾。同理,short就是以0b10结尾,double就是以0b1000结尾。需要注意的是对齐大小都是2的整数次幂。

int main(int argc, char const *argv[])
{

    static_assert(alignof(long) == 4);

    alignas(4) long x;

    std::bitset<64> address = (uint64_t)std::addressof(x);

    // 0000000000000000000000000111001010111111010111111111101100111100
    std::cout << address.to_string() << '\n';

    return 0;   
}

知道了基础类型之后,我们来看结构体的类型:

struct F1
{
                  //   alignment  sizeof 
    int8_t i8;    //      1          1
    int16_t i16;  //      2          2
    double f64;   //      8          8
};

static_assert(alignof(F1) == 8);
static_assert(sizeof(F1) == 16);

1、如何计算对齐大小?

结构体类型的对齐大小为最大的成员字段的对齐大小,F1结构体中f64字段对齐大小是最大的,所以F1的对齐大小和double保持一致。


2、如何计算结构体大小

int main(int argc, char const *argv[])
{

    alignas(8) F1 f;

    std::bitset<64> address;
    
    // 0b000000
    address = (uint64_t)std::addressof(f);
    std::cout << address.to_string() << '\n';

    // 0b000000
    address = (uint64_t)std::addressof(f.i8);
    std::cout << address.to_string() << '\n';

    // 0b000010
    address = (uint64_t)std::addressof(f.i16);
    std::cout << address.to_string() << '\n';
    
    // 0b001000
    address = (uint64_t)std::addressof(f.f64);
    std::cout << address.to_string() << '\n';

    return 0;   
}

我们将f及其所有字段的地址的最后6比特打印出来。由于f是按照8字节对齐,所以f的二进制地址最后应该是0b1000结尾。i8是按照1字节对齐,它是结构体的第一个字段,其地址和f地址一样。i16是按照2字节对齐的,所以他的地址应该是以0b100结尾,而f的地址0b000000在安放完一个i8后的下一个以0b100结尾的地址是0b000010,所以i16会被安放在0b000010。f64是按照8字节对齐,而f的地址0b000000在安放完一个i8和一个i16后的下一个以0b1000结尾的地址是0b001000,所以f64会被安放在0b001000,如图所示:

图1

模拟布局

为了更好地了解结构体的内存布局,接下来我们来模拟一下。

template <typename TypeList, typename SizeSequence> 
struct layout_impl;

template <typename... Ts, size_t... Sizes> 
struct layout_impl<std::tuple<Ts...>, std::index_sequence<Sizes...>>
{ 
    // ...
};

// Ts... 是我们结构体每一个字段的类型
template <typename... Ts>
struct layout : layout_impl<
    std::tuple<Ts...>, std::make_index_sequence<sizeof...(Ts)>>
{
};

首先是构造函数,我们使用一个数组来记录每个字段的数量,在构造时提供每个字段的数量即可。

template <typename... Ts, size_t... Sizes> 
struct layout_impl<std::tuple<Ts...>, std::index_sequence<Sizes...>>
{
    // C++目前不允许layout_impl(size_t... sizes)这种写法,我们利用模板来完成这个功能
    constexpr explicit layout_impl(detail::to_size_t<Sizes>... sizes)
      : m_sizes{ sizes... } {}
    std::array<size_t, NumSizes> m_sizes;
};

/*
struct F1
{
    int8_t i8;    
    int16_t i16;  
    double f64;   
}; => layout<int8_t, int16_t, double>(1, 1, 1)

struct TreeNode
{
    int value;
    TreeNode* children[2];
}; => layout<int, TreeNode*>(1, 2);
*/

其次是结构体的对齐大小,结构体的对齐大小和对齐大小最大的那个字段保持一致。

static constexpr size_t alignment() 
{
    return std::ranges::max({ alignof(Ts)... });
}

再然后是计算每一个字段的offset。

在计算某个字段的时候,我们需要知道上一个字段的offset和size,然后按照当前字段的对齐大小进行对齐。以F1的第二个字段i16为例,上一个字段是i8,由于他是第一个字段,offset为0,size是1个字节,我们可以按照如下公式计算:

\[ next = (current + align - 1) \mod align \]

举个例子,假设我们是十进制并且按照10字节对齐,当前面有1个字节被占用的时候,我们需要填充9个字节,当前面有2个字节被占用的时候,我们需要填充8个字节,依次类推,当前面有10个字节都被占用的时候,我们不需要填充字节。

constexpr size_t align(size_t n, size_t m) 
{ 
    assert(std::popcount(m) == 1 && "m should be power of two.");
    return (n + m - 1) & ~(m - 1); 
}

template <size_t N>
constexpr size_t offset() const
{
    static_assert(N < NumSizes);
    if constexpr (N == 0)
    {
        return 0;
    }
    else
    {
        const auto last_type_size = sizeof(std::tuple_element_t<N - 1, std::tuple<Ts...>>);
        const auto current_type_alignment = alignof(std::tuple_element_t<N, std::tuple<Ts...>>);
        return detail::align(offset<N - 1>() + last_type_size * m_sizes[N - 1], current_type_alignment);
    }
}

最后是结构体的大小。我们只需要知道最后一个字段的offset,大小以及填充字节大小就可以了,需要注意的是,最后一个字节也是可能会填充的,这样在申请数组的时候才能保证每一个元素都是对齐的。

constexpr size_t alloc_size() const
{
    return offset<NumTypes - 1>() + sizeof(std::tuple_element_t<NumTypes - 1, std::tuple<Ts...>>) * m_sizes[NumTypes - 1];
}

题外话

结构体的字段的分布对布局是有影响的,如果我们更改F1字段的顺序,那么它的大小可能会发生变化。

struct F2
{
                  //   alignment  sizeof 
    int8_t i8;    //      1          1
    double f64;   //      8   
    int16_t i16;  //      2          2
};

static_assert(alignof(F2) == 8);
static_assert(sizeof(F2) == 24);

如果您想更好地回顾关于结构体对齐的知识,可以参见https://www.youtube.com/watch?v=SShSV_iV1Ko

参考文献

[1]维基百科:https://en.wikipedia.org/wiki/Data_structure_alignment

标签:std,layout,字节,C++,字段,对齐,size,结构
From: https://www.cnblogs.com/MasterYan576356467/p/17338342.html

相关文章

  • C++基础2: 优化C函数
    1.缺省参数什么是缺省参数缺省参数是声明或者定义函数时为函数的参数指定一个默认值,如果函数调用时没有传入实参,那么这个默认值会被当做实参,如下例子函数调用时,传入参数1,a=1,不传入参数,默认a=0,这里的a就是一个缺省参数缺省参数的分类缺省参数分全缺省和半缺省......
  • 数据结构 玩转数据结构 13-1 红黑树与2-3树
    0课程地址https://coding.imooc.com/lesson/207.html#mid=15086 1重点关注1.1红黑树的特性  1.22-3树的特性满足二叉树性质2-3树是一棵绝对平衡的树  2课程内容2.12-3树定义每个节点有两个或三个子节点的二......
  • IO流的体系结构和字节输出流
        ......
  • 双端队列数据结构
    双端队列是一种数据结构,也被称为deque或double-endedqueue。它类似于队列,但它允许从队列的两端添加或删除元素,而不仅仅是队列的一端。双端队列可以用数组或链表实现。如果使用数组实现,它可以使用循环数组的方式,使得在头尾进行插入和删除的操作可以在常数时间内完成。如果使用链......
  • c++打卡第四天
    一、问题描述。有一对兔子,第三个月开始每月生一对兔子,刚出生的兔子经过三个月又可以生一对兔子,问从1月开始到n月,每月兔子的数量。二、设计思路。①、第一二个月都是一对兔子,第三个月是2对,3个月是三对,第四个月就是5对。②、由此可知,这个月兔子对数的总量等于前一个月和前两个月......
  • C/C++《程序设计基础(C语言)课程设计》[2023-04-20]
    C/C++《程序设计基础(C语言)课程设计》[2023-04-20]《程序设计基础(C语言)课程设计》课程说明及动员《程序设计基础(C语言)课程设计》指导教师组目录课程目的>>课程要求>>团队题目>>实施方案>>课程设计报告>>考核与成绩评定方法>>本学期实施安排>>其他说明课程目的......
  • cuda软硬件结构
     我们简单分析一下,硬件角度,主要分为计算机硬件(简单来说就是电脑)和显卡硬件(也就是GPU),这里计算机硬件为host端,显卡硬件为GPU端。接着,我们从图片中计算机硬件来进行分析。这里,我们统一采用Ubuntu系统(Ubuntu18.04或者Ubuntu20.04)都可以,这里我们不采用Windows系统,因为Windows系统运......
  • C++课本第四章例题
    时钟类的完整例题#include<iostream>usingnamespacestd;classClock{private:inthour,minute,second;public:voidsetTime(inthour=0,intminute=0,intsecond=0);voidshowTime();};voidClock::setTime(intnewH,intnewM,i......
  • 构建树状结构工具类
    实体类@DatapublicclassTreeNode{/**节点ID*/privateIntegerid;/**父节点ID:顶级节点为0*/privateIntegerparentId;/**节点名称*/privateStringlabel;/**子节点*/privateList<TreeNode>children;/**......
  • C++黑马程序员——P185-188. STL初识
    P185.STL初识——STL的基本概念P186.STL初识——vector存放内置数据类型P187.STL初识——vector存放自定义数据类型P188.STL初识——容器嵌套容器P185.STL的基本概念STL,StandardTemplateLibrary,标准模板库STL:为了提高代码的复用性,提供一套标准的数据结构和算法STL......