C++中的内存对齐介绍

网上有很多介绍字节对齐或数据对齐或内存对齐的文章,虽然名字不一样,但是介绍的内容大致都是相同的。这里以内存对齐相称。注:以下内容主要来自网络。

内存对齐,通常也称为数据对齐,是计算机对数据类型合法地址做出了一些限制,要求某种类型对象的地址必须是某个值K(通常使2、4、8、16、32或64)的倍数。

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

内存对齐原因:

(1). 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

(2). 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

对齐值必须是2的幂次方,如1, 2, 4, 8, 16。如果一个变量按n字节对齐,那么该变量的起始地址必须是n的倍数。

每个特定平台上的编译器都有自己默认的”对齐系数”,可以通过设置#pragma pack(n),告诉编译器,所有的对齐都是按照n的整数倍对齐。

在结构体中,整个结构的大小必须是其中最大字段大小的整数倍。

为了让处理器快速读写内存里面的数据,默认情况,编译器会把:

(1). 1个字节的变量,例如char类型的变量,放在任意地址的位置上;

(2). 2个字节的变量,例如short类型的变量,放在2的整数倍的地址上;

(3). 4个字节的变量,例如long/float类型的变量,放在4的整数倍地址上;

(4). 8个字节的变量,例如long long/uint64_t或double类型的变量,放在8的整数倍地址上;

(5). 16个字节的变量,放在8的整数倍地址上,因为默认的对齐方式是 8。

变量在内存里面的顺序,和定义变量的顺序相同。为了符合对齐方式,就会在变量之间加入填充字节(padding),让后面的变量放在按照对齐方式的规则的地址上

strcut/class/union内存对齐规则:

1. 没有#pragma pack宏的对齐规则:

(1). 结构体的起始存储位置必须是能够被该结构体中最大的数据类型所整除。

(2). 每个数据成员存储的起始位置是自身大小的整数倍(比如int在32位机为4字节,则int型成员要从4的整数倍地址开始存储)。

(3). 结构体总大小(也就是sizeof的结果),必须是该结构体成员中最大的对齐模数的整数倍。若不满足,会根据需要自动填充空缺的字节。

(4). 结构体包含另一个结构体成员,则被包含的结构体成员要从其原始结构体内部最大对齐模数的整数倍地址开始存储(比如struct a里存有struct b,b里有char,int,double等元素,那b应该从8的整数倍开始存储)。

(5). 结构体包含数组成员,比如char a[3],它的对齐方式和分别写3个char是一样的,也就是说它还是按一个字节对齐。如果写:typedef char Array[3], Array这种类型的对齐方式还是按一个字节对齐,而不是按它的长度3对齐。

(6). 结构体包含共用体成员,则该共用体成员要从其原始共用体内部最大对齐模数的整数倍地址开始存储。

2. 存在#pragma pack宏的对齐:

(1). #pragma pack (n) // 编译器将按照n个字节对齐 

(2). #pragma pack () //取消自定义字节对齐方式

可以通过C++11中的alignas函数来指定类型、对象或变量按多少字节对齐,可以通过alignof函数来判断类型、对象或变量是按多少字节对齐的。

下面是从其他文章中copy的测试代码,详细内容介绍可以参考对应的reference:

memory_alignment.cpp内容如下:

#include "memory_alignment.hpp"
#include <iostream>
#include <cstdlib>

//#pragma pack(1) // use #pragma pack set memory alignment

namespace memory_alignment_ {

//////////////////////////////////////////////////////
int test_memory_alignment_1()
{

{ // struct	
	typedef struct A {char c;} A;
	typedef struct B {int i;} B;
	typedef struct C {double d;} C;
	typedef struct D {char c; int i;} D;
	typedef struct E {char* p;} E; // 32bits is 4, 64bits is 8
	typedef struct F {char* p; int* p2;} F;
	typedef struct G {char c1; char c2; char c3;} G;
	typedef struct H {char c; int* p;} H;
	typedef struct I {char c; int* p; int i;} I;
	typedef struct J {char c; int i; int* p;} J;
	typedef struct K {} K; // C++ size is 1, but C is 0
	fprintf(stdout, "size: A: %d, B: %d, C: %d, D: %d, E: %d, F: %d, G: %d, H: %d, I: %d, J: %d, K: %d\n",
		sizeof(A), sizeof(B), sizeof(C), sizeof(D), sizeof(E), sizeof(F), sizeof(G), sizeof(H), sizeof(I), sizeof(J), sizeof(K));

	fprintf(stdout, "size: short: %d, long: %d, float: %d, long long: %d, double: %d, uint64_t: %d\n",
		sizeof(short), sizeof(long), sizeof(float), sizeof(long long), sizeof(double), sizeof(uint64_t));
}

	return 0;
}

//////////////////////////////////////////////////////////
// reference: https://stackoverflow.com/questions/17091382/memory-alignment-how-to-use-alignof-alignas
int test_memory_alignment_2()
{
{
	// alignas: 类型或对象或变量按指定的字节对齐
	// Alignment of 16 means that memory addresses that are a multiple of 16 are the only valid addresses.
	alignas(16) int a[4];
	alignas(1024) int b[4];
	fprintf(stdout, "address: %p\n", a);
	fprintf(stdout, "address: %p\n", b);

	// alignof: 查询指定类型的对齐要求,返回size_t类型值
	if (alignof(a) != 16 || (unsigned long long)a % 16 != 0) {
		fprintf(stderr, "a must be 16 byte aligned.\n");
		return -1;
	}

	if (alignof(b) != 1024 || (unsigned long long)b % 1024 != 0) {
		fprintf(stderr, "b must be 1024 byte aligned.\n");
		return -1;
	}
}

{
	// every object of type sse_t will be aligned to 16-byte boundary
	struct alignas(16) sse_t { float sse_data[4]; };
 	// the array "cacheline" will be aligned to 128-byte boundary
	alignas(128) char cacheline[128];
}

	return 0;
}

//////////////////////////////////////////////////////////////
// reference: https://en.cppreference.com/w/cpp/language/alignof
struct Foo {
    int   i;
    float f;
    char  c;
};
 
struct Empty {};
 
struct alignas(64) Empty64 {};

int test_memory_alignment_3()
{
	std::cout << "Alignment of"  "\n"
		"- char             : " << alignof(char)    << "\n"
		"- pointer          : " << alignof(int*)    << "\n"
		"- class Foo        : " << alignof(Foo)     << "\n"
		"- empty class      : " << alignof(Empty)   << "\n"
		"- alignas(64) Empty: " << alignof(Empty64) << "\n";

	return 0;
}

//////////////////////////////////////////////////////////////////////
// reference: https://msdn.microsoft.com/en-us/library/dn956973.aspx
int test_memory_alignment_4()
{
	struct x_ {
		char a;     // 1 byte  
		int b;      // 4 bytes  
		short c;    // 2 bytes  
		char d;     // 1 byte  
	} MyStruct;

	// The compiler pads this structure to enforce alignment naturally.
	// The following code example shows how the compiler places the padded structure in memory:Copy
	// Shows the actual memory layout  
	/*struct x_ {
		char a;            // 1 byte  
		char _pad0[3];     // padding to put 'b' on 4-byte boundary  
		int b;            // 4 bytes  
		short c;          // 2 bytes  
		char d;           // 1 byte  
		char _pad1[1];    // padding to make sizeof(x_) multiple of 4  
	} MyStruct; */

	// 1. Both declarations return sizeof(struct x_) as 12 bytes.
	// 2. The second declaration includes two padding elements:
	// 3. char _pad0[3] to align the int b member on a four-byte boundary
	// 4. char _pad1[1] to align the array elements of the structure struct _x bar[3];
	// 5. The padding aligns the elements of bar[3] in a way that allows natural access.

	return 0;
}

} // namespace memory_alignment_

CMakeLists.txt内容如下:

PROJECT(CppBaseTest)
CMAKE_MINIMUM_REQUIRED(VERSION 3.0)

# 支持C++11
SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g -Wall -O2 -std=c11")
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}  -g -Wall -O2 -std=c++11")
# 支持C++14, when gcc version > 5.1, use -std=c++14 instead of c++1y
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}  -g -Wall -O2 -std=c++1y")

MESSAGE(STATUS "project source dir: ${PROJECT_SOURCE_DIR}")
SET(PATH_SRC_FILES ${PROJECT_SOURCE_DIR}/./../../demo/CppBaseTest)
MESSAGE(STATUS "path src files: ${PATH_SRC_FILES}")

# 指定头文件的搜索路径
INCLUDE_DIRECTORIES(${PATH_SRC_FILES})

# 递归查询所有匹配的文件:*.cpp
FILE(GLOB_RECURSE CPP_LIST ${PATH_SRC_FILES}/*.cpp)
FILE(GLOB_RECURSE C_LIST ${PATH_SRC_FILES}/*.c)
#MESSAGE(STATUS "cpp list: ${C_LIST}")

# 编译可执行程序
ADD_EXECUTABLE(CppBaseTest ${CPP_LIST} ${C_LIST})
# 用来为target添加需要链接的共享库,指定工程所用的依赖库,包括动态库和静态库
TARGET_LINK_LIBRARIES(CppBaseTest pthread)

build.sh脚本内容如下:

#! /bin/bash

real_path=$(realpath $0)
dir_name=`dirname "${real_path}"`
echo "real_path: ${real_path}, dir_name: ${dir_name}"

new_dir_name=${dir_name}/build
mkdir -p ${new_dir_name}
cd ${new_dir_name}
cmake ..
make

cd -

编译及测试方法如下:首先执行build.sh,然后再执行./build/CppBaseTest即可。

GitHubhttps://github.com/fengbingchun/Messy_Test  

猜你喜欢

转载自blog.csdn.net/fengbingchun/article/details/81270326
今日推荐