首页 > 系统相关 >内存对齐

内存对齐

时间:2024-09-07 23:15:03浏览次数:6  
标签:std 读取 int 内存 对齐 字节

内存对齐

关于内存对齐,看我 - 简书 (jianshu.com)

深入了解 | 内存对齐之 alignof、alignas 、aligned_storage、align 深度剖析 - 知乎 (zhihu.com)

什么是内存对齐

计算机中内存空间是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是:在访问特定类型变量的时候通常在特定的内存地址访问,这就需要对这些数据在内存中存放的位置有限制,各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。

内存对齐是编译器的管辖范围。表现为:编译器为程序中的每个“数据单元”安排在适当的位置上。

为什么要内存对齐

为了解释这个问题,我们先要了解一下处理器是如何读取内存的?

我们如果把内存看做是简单的字节数组,比如在C语言中,char *就可表示一块内存。那么或许我们会认为,它的内存读取方式可以按照1byte顺序读取,如下图。

然而,尽管内存是以字节为单位,但是大部分处理器并不是按字节块来存取内存的,这取决于数据类型和处理器的设置;它一般会以双字节,四字节,8字节,16字节甚至32字节的来存取内存,我们将上述这些存取单位称为内存存取粒度.

现在我们知道,计算机的处理器是以一定大小的块来进行读取的,这作为我们的前提条件,那么为了解释为什么要内存对齐?我们不妨先看一看不对齐的情况会出现什么问题?

对齐跟数据在内存中的位置有关。如果一个变量的内存地址刚好位于它本身长度的整数倍,他就被称做自然对齐。例如一个整型变量(占4字节)的地址为0x00000016,那它就是自然对齐的。

现在假设一个整型变量(4字节)不是自然对齐的,它的起始地址落在0x00000002(图中蓝色区域),处理器想要访问它的值,按照4字节的块进行读取,从图中的0x0起读,读取4字节大小,读到0x3

这样的一次读取之后,我们并不能取到我们要访问的整型数据,紧接着处理器会继续再往下读,偏移4个字节,从0x4开始,读到0x7。

到这里,处理器才能读取到了我们需要访问的内存数据,当然这中间还存在剔除与合并的过程。

所以,在此例中,当整型变量起始地址落在0x2时(不对齐),处理器需要两次读取才能取到我们要访问的内容。

那如果是对齐的呢?

显然,如果是对齐的,对于本例,仅需读取1次,我们便可以读取到目标数据。

可见,对齐与否会影响到我们的读取效率。

同时:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取,而不是内存中任意地址都是可以读取的。比如有些架构的CPU在访问一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐.其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。也正是由于只能在特定的地址处读取数据,所以在访问一些数据时,对于访问未对齐的内存,处理器可能需要进行多次访问;而对于对齐的内存,只需要访问一次就可以。

这就是为什么要内存对齐的原因。内存对齐不仅便于CPU快速访问,同时合理的利用字节对齐可以有效地节省存储空间。我们可以同时对比一下,不同内存存取粒度对同一任务的不同影响。设定一个相同的任务:分别从Address0 和 Address1 中从地址0读取4个字节到处理器的寄存器中。

  • 先看单字节粒度情况

两图中左侧表示内存,右侧表示寄存器,中间的箭头表示读取的过程。因为是单字节存取粒度,读取内存是按照1个字节进行访问的,所以对于Address0 来讲,要想从0的位置读取4个字节,需要读取4次,对于 Address1 也是一样的。即使他不是内存对齐的也无妨。

  • 再看双字节粒度情况

从Address0 读取4个字节,相比于存取粒度为1字节的处理器,存取次数变成了一半,只需读取2次。由于每个内存访问都需要固定的开销,因此最小化访问次数确实可以提高性能。同时Address0 是内存对齐的(数据的起始位置落在0的位置上),所以第一次读取地址01,第二次读取地址23即可取到目标数据。

但是,从Address1 读取时。由于该地址未均匀地落在处理器的内存访问边界上(Address1 中寄存器黑框区域是该数据的内存地址区域,起始位置为1,不是对齐的),该处理器去取数据时,要先从0地址开始读取第一个2字节块(01),剔除不想要的字节(0地址),然后从地址2开始读取下一个2字节块(23),再从地址4开始读取下一个2字节块(45),剔除不想要的字节(地址5)。这样读取3次之后将最后留下的3块数据合并放入寄存器,才能取到目标数据。

  • 四字节粒度情况呢?

具有四字节粒度的处理器从Address0 读取时,可以一次读取地址0123 就从对齐的地址中提取四个字节。

然而从Address1 读取时,因为是不对齐的,读取地址0123,剔除0地址,继而读取地址4567,剔除地址5、地址6、地址7 ,这样读取2次后将留下的2块数据合并放入寄存器,取到目标数据。

从双字节粒度和四字节粒度中可见,对于没有对齐的内存,需要做更多次的读取与剔除和合并的过程。这显然是降低效率的。

同时我们还需要注意到一点:对于对齐的内存,不同的存取粒度也会影响到存取效率。粒度小则存取次数多,粒度大则浪费空间。所以在每个特定平台上的编译器都有自己的默认存取粒度。

了解了内存对齐以及原因,我们继续看一下内存对齐的原则是什么。

内存对齐规则

  • 对于标准数据类型:它的地址只要是它的长度的整数倍就行了
  • 对于结构体:在结构体中,编译器为结构体的每个成员按其自然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构体的地址相同。具体规则如下:
    • 第一个成员在结构体变量偏移量为0 的地址处,也就是第一个成员必须从头开始。
    • 以后每个成员相对于结构体首地址的 offset 都是该成员大小的整数倍,如有需要编译器会在成员之间加上填充字节。
    • 结构体的总大小为 最大对齐数的整数倍(每个成员变量都有自己的对齐数),如有需要编译器会在最末一个成员之后加上填充字节。
    • 如果嵌套结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(包含嵌套结构体的对齐数)的整数倍。

举个栗子:

struct test_t {
  int   a;
  long  b;
  short c;
};
  • 第一个成员为int类型,占4字节,内存分布 00 01 02 03 ,用红色表示;
  • 第二个成员为long类型,占8字节,此时内存的的偏移量04不是8的整数倍,所以要填充字节(绿色表示填充字节),到0x7的位置,然后将long类型的数据b写入内存(黄色);
  • 第三个成员是short类型,占2个字节,此时内存的偏移量16是short类型所占字节数的整数倍,所以直接写入内存(用蓝色表示)

至此,结构体内的数据数据成员已对齐,但是当前结构体的总大小为18,不满足规则3,所以需在最末的成员后面填充6个字节,使其总大小为24。

如果将结构体中的short类型与long类型换一下,

struct test_t {
  int   a;
  short b;
  long  c;
};

结果为16。可见,对于成员相同的结构体,如果改变成员的顺序,对于结构体所占空间的大小是会产生影响的,所以,我们不但要了解内存对齐,还是正确的利用内存对齐。

对于结构体嵌套结构体,在规则4中已经给出,这里的图就不再画了,聪明的你看到这里一定可以得到正确的答案。

  • 各成员变量存放的起始地址, 相对于结构的起始地址的偏移量 ,必须为该变量的类型所占用的字节数的倍数;
  • 各成员变量在存放的时候根据在结构中出现的顺序依次申请空间, 同时按照上面的对齐方式调整位置, 空缺的字节自动填充
  • 同时为了确保结构的大小为结构的字节边界数(即该结构中占用最大的空间的类型的字节数)的倍数,所以在为最后一个成员变量申请空间后 还会根据需要自动填充空缺的字节

C++的内存对齐api

alignof

alignof 是 C++11 引入的一个关键字,用来获取类型的对齐要求(以字节为单位)。在内存中,不同类型的数据有不同的对齐方式,alignof 可以帮助你知道某个类型需要怎样的对齐。

#include <iostream>
#include <cstddef>  // for alignof

struct MyStruct {
    char c;
    int i;
    double d;
};

int main() {
    std::cout << "Alignment of char: " << alignof(char) << " bytes\n";
    std::cout << "Alignment of int: " << alignof(int) << " bytes\n";
    std::cout << "Alignment of double: " << alignof(double) << " bytes\n";
    std::cout << "Alignment of MyStruct: " << alignof(MyStruct) << " bytes\n";
    return 0;
}

输出:

Alignment of char: 1 bytes
Alignment of int: 4 bytes
Alignment of double: 8 bytes
Alignment of MyStruct: 8 bytes

alignas

alignas 是 C++11 引入的关键字,用于指定变量或类型的对齐方式。与 alignof 不同,alignas 是用来设定对齐,而不是查询对齐。可以用来确保某个变量、结构体或类按照指定的字节对齐方式存储,这在性能优化或硬件交互时非常有用。

#include <iostream>

struct alignas(16) MyStruct {
    char c;
    int i;
    double d;
};

int main() {
    MyStruct s;
    std::cout << "Alignment of MyStruct: " << alignof(MyStruct) << " bytes\n";
    std::cout << "Address of s: " << &s << '\n';
    return 0;
}

输出:

Alignment of MyStruct: 16 bytes
Address of s: 0x... (地址将是16的倍数)

说完alignas的基础用法,下面说下使用alignas时的注意事项,即alignas(alignment)中的alignment也不是随意写的,对于类型T,需要满足如下两个条件。

  1. alignment >= alignof(T)
  2. alignment == pow(2, N)

std::aligned_storage

std::aligned_storage 是一个模板类,定义于 C++11 中,用于创建满足特定大小对齐要求的未初始化内存块。它主要用于需要手动控制内存布局的场景,特别是在一些低级别的内存管理或者与硬件相关的程序中。

// in <type_traits>
template< std::size_t Len, 
          std::size_t Align = /*default-alignment*/ >
struct aligned_storage;

std::aligned_storage 提供了一种方便的方式来分配对齐内存,而无需自己管理复杂的对齐逻辑。它常常被用于创建未初始化的对象存储空间,稍后可以使用 placement new 在该内存上构造对象。

template<typename T, size_t N>
class StaticVector {
public:
    StaticVector() { 
      std::cout << alignof(T) << "/" << sizeof(T)<< std::endl;
      for (int idx = 0; idx < N; ++idx) { 
        std::cout << &data[idx] << std::endl;
      }
    }

    ~StaticVector() {
      for(size_t pos = 0; pos < m_size; ++pos) {
        reinterpret_cast<T*>(data+pos)->~T();
      }
    }

    template<typename ...Args> 
    void emplace_back(Args&&... args) {
      if(m_size >= N) {
        throw std::bad_alloc{};
      }
      new(data+m_size) T(std::forward<Args>(args)...);
      ++m_size;
    }

    const T& operator[](size_t pos) const {
      return *reinterpret_cast<const T*>(data+pos);
    }

private:
  // std::aligned_storage<sizeof(T), alignof(T)>::type data[N]; // C++11
  std::aligned_storage_t<sizeof(T), alignof(T)> data[N];        // c++14
  size_t m_size = 0;
};

StaticVector的使用如下:

struct alignas(32) Foo { 
  char c;
  int i1; 
  int i2;
  long l;
};

int main(int argc, char const *argv[]) {
    StaticVector<std::string, 2> v1;
    v1.emplace_back(5, '*');
    std::cout << v1[0] << '\n';

    StaticVector<Foo, 2>v2;
}

std::align

std::aligned_storage 是一个静态的内存对齐分配器,即在类std::aligned_storage对象构造完时,就已满足设定内存大小、内存对齐要求,但是如果现在有一块内存,想从中取出一块符合某对齐要求的内存,咋办?

此时就可以使用std::align函数,其函数原型如下:

/// @param  alignment 是想要分配的内存符合的内存对齐大小
/// @param  size 想要分配内存的大小
/// @param  ptr 是个输入输出参数,输入时指向待使用的内存,输出时调整为符合alignment对齐要求的内存地址
/// @param  space 是ptr指向的内存剩余的空间
/// @return 如果 ptr 经过调整后能满足大小为 alignment 的对齐要求,则返回ptr的值,否则返回 nullptr
void* align( std::size_t alignment,
             std::size_t size,
             void*& ptr,
             std::size_t& space);

Arena内已有一块缓冲区buffer,每次调用AlignedAllocate<T>(size_t alignment)函数时,即需要从buffer中取出大小为sizeof(T)的一块内存ptrAlignedAllocate函数的输入参数alignment指定了获得的内存ptr满足的内存对齐要求。

template <size_t N>
struct Arena {
  char buffer[N];
  void* ptr;
  size_t size;

  Arena() : ptr(buffer), size(N) { }

  /// @return 返回的指针满足大小为 alignment 的内存对齐要求
  template <typename T>
  T* AlignedAllocate(size_t alignment = alignof(T)) {
      std::cout << "ptr: " << reinterpret_cast<void*>(ptr) << ", ";
      if (std::align(alignment, sizeof(T), ptr, size)) {
          T* result = reinterpret_cast<T*>(ptr);
          ptr = (char*)ptr + sizeof(T);
          size -= sizeof(T);
          return result;
      }
      // 若无,则返回 nullptr
      return nullptr;
  }
};

下面是测试:

int main(int argc, char const *argv[]) {
    Arena<64> arena;

    char* p1 = arena.AlignedAllocate<char>();
    if (p1) *p1 = 'a';
    std::cout << "allocated a char at " << (void*)p1 << '\n';

    int* p2 = arena.AlignedAllocate<int>();
    if (p2) *p2 = 1;
    std::cout << "allocated an int at " << (void*)p2 << '\n';

    int* p3 = arena.AlignedAllocate<int>(32);
    if (p3) *p3 = 2;
    std::cout << "allocated an int at " << (void*)p3 << '\n';
}

从下面的输出可以看出,AlignedAllocate 函数返回的内存地址都是符合设定的内存对齐要求的。

$ g++ align.cc -o align && ./align 
ptr: 0x16fc2b4b8, allocated a char at 0x16fc2b4b8     # 1 byte 内存对齐,指针无须调整
ptr: 0x16fc2b4b9, allocated an int at 0x16fc2b4bc     # 4 byte 内存对齐,指针调整了 3 个字节
ptr: 0x16fc2b4c0, allocated an int at 0x16fc2b4c0     # 32 byte 内存对齐,指针无须调整

虚共享

虚共享false sharing)是多线程编程中的一种性能问题,通常发生在多核处理器上。它指的是多个线程访问不同的变量,但这些变量共享同一个缓存行,导致不必要的缓存同步,从而影响性能。

  • 缓存行:CPU 缓存是按 缓存行 的单位进行读写操作的,通常每个缓存行大小为 64 字节。这意味着即使两个线程操作的是不同的变量,只要它们在同一个缓存行中,这两个线程就会不必要地竞争访问相同的缓存行,导致缓存同步和失效(cache invalidation)。
  • 虚共享问题:当两个或多个线程各自修改不同的变量时,如果这些变量位于同一个缓存行,线程每次修改变量时,都会导致缓存同步机制(比如通过MESI协议)触发,强制其他处理器上的缓存失效,即便这些线程并没有真正共享数据。这种情况就叫做 虚共享。虽然数据看似没有实际共享,但缓存的共享行为导致了性能下降。
#include <iostream>
#include <thread>
#include <vector>
#include <chrono>

constexpr int ITERATIONS = 1'000'000;

struct SharedData {
    alignas(64) int data1; // 使用 alignas(64) 来防止虚共享
    alignas(64) int data2;
};

void threadFunc1(SharedData &shared) {
    for (int i = 0; i < ITERATIONS; ++i) {
        ++shared.data1;
    }
}

void threadFunc2(SharedData &shared) {
    for (int i = 0; i < ITERATIONS; ++i) {
        ++shared.data2;
    }
}

int main() {
    SharedData shared = {0, 0};

    auto start = std::chrono::high_resolution_clock::now();

    std::thread t1(threadFunc1, std::ref(shared));
    std::thread t2(threadFunc2, std::ref(shared));

    t1.join();
    t2.join();

    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();

    std::cout << "Execution time: " << duration << "ms\n";
    return 0;
}

在上面的例子中,data1data2 本质上是两个独立的变量,分别被两个线程操作。然而,如果这两个变量恰好在同一个缓存行中,会导致虚共享。为了解决这个问题,我们通过 alignas(64) 使 data1data2 分别位于不同的缓存行中,避免虚共享带来的性能问题。

标签:std,读取,int,内存,对齐,字节
From: https://www.cnblogs.com/sfbslover/p/18402305

相关文章

  • python学习总结之内存处理
    1.引用计数法注:类似于java,这个系统自动回收垃圾对象,明显有循环引用的弊端。代码例子importsysimportpsutilimportosimportgcprint(gc.get_threshold())defshowMemSize(tag):pid=os.getpid()p=psutil.Process(pid)info=p.memory_full_info()memory=i......
  • # 结构体成员定义的顺序也会影响结构体的大小,内存对齐,内存填充
    结构体成员定义的顺序也会影响结构体的大小,内存对齐,内存填充usingSystem;usingSystem.Runtime.InteropServices;structStrcutOne{publicintb;//4bytespublicbytea;//1publicbytec;//1//4+1+1+2(在填充两个2个字节)=8字节}struct......
  • Promise resolve reject 一直不执行会不会导致内存泄漏
    如果一个Promise一直不resolve或reject,它本身不会直接导致内存泄漏。这是因为Promise对象在其状态变为fulfilled(已解决)或rejected(已拒绝)之后就会变成不可变的状态,并且Promise本身并不会持有对大量数据的引用。然而,有几个方面需要注意:事件监听器和定时器:如果Pr......
  • excel姓名两个字和三个字对齐怎么做?
    下面设计学徒自学网给大家介绍一下excel2个字和3个字人名对齐打开一个有人名的Excel文件。选中有人名的单元格后右键选择设置单元格格式。选择上方对齐。文章源自四五设计网-https://www.45te.com/50521.html选择水平对齐下拉列表内的分散对齐(缩进)。点击确定即可。效......
  • 深入理解动态内存(一):动态内存使用常见问题
    目录对NULL指针的解引用操作对动态开辟空间的越界访问对非动态开辟内存使用free释放使用free释放⼀块动态开辟内存的⼀部分对同⼀块动态内存多次释放动态开辟内存忘记释放(内存泄漏)对NULL指针的解引用操作#include<stdio.h>#include<stdlib.h>intmain(){ int*p......
  • Linux 性能优化(网络、磁盘、内存、日志监控)
    1、CPU性能监控1.2、平均负载基础平均负载是指单位时间内,系统处于可运行状态和不可中断状态的平均进程数,也就是平均活跃进程数,它和CPU使用率并没有直接关系。平均负载其实就是平均活跃进程数。平均活跃进程数,直观上的理解就是单位时间内的活跃进程数。查看cpu个数:grep'modelnam......
  • c语言内存函数
    今天来学习C语言中的内存函数目录1.memcpy代码形式示例运行结果2.memmove代码形式示例运行结果3.memset代码形式示例运行结果4.memcmp形式示例运行结果![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/b7d8d59577b248deaa7b869d014d8b4f.png#pic_center)5......
  • 什么是内存分页和分段
    内存分页和分段是操作系统用于管理内存的一种技术,旨在提高内存的使用效率和安全性。它们各自有不同的结构和目的。1.内存分页(Paging)概述内存分页是一种将物理内存划分为固定大小的块(称为页,通常为4KB)和将逻辑地址空间划分为相同大小的块(称为页表)的机制。分页允许不连续的物理内......
  • 什么是栈内存和堆内存
    栈内存和堆内存是计算机程序运行时用来管理内存的两种不同区域。它们各自有不同的特性和用途。以下是对栈内存和堆内存的详细解释:1.栈内存(StackMemory)定义栈内存是一种用于存储局部变量和函数调用信息的内存区域。栈是先进后出(LIFO,LastInFirstOut)的结构。特点分配与释......
  • 内存管理-34-内存回收-shrinker的注册和调用
    基于msm-5.4一、简介当存在内存压力时,会调用shrinker的count_objects()和scan_objects()进程内存回收操作。二、注册逻辑1.注册ashmem_init//ashmem.cregister_shrinker(&ashmem_shrinker)//vmscan.cregister_shrinker_prepared(shrinker)......