有没有一种聪明的方法可以避免在 C++ 中使用嵌套 类 进行额外填充?
Is there a clever way of avoiding extra padding with nested classes in C++?
这些结构 align1
和 align2
包含相同的数据,但 align1
由于嵌套布局而具有更多填充。
如何获得 align2
的内存节省对齐方式,同时还使用 align1
中的嵌套结构?
int main() {
struct align1 {
struct {
double d; // 8 bytes
bool b1; //+1 byte (+ 7 bytes padding) = 16 bytes
} subStruct;
bool b2; //+1 byte (+ 7 bytes padding) = 24 bytes
};
struct align2 {
double d; // 8 bytes
bool b1, b2; //+2 byte (+ 6 bytes padding) = 16 bytes
};
std::cout << "align1: " << sizeof(align1) << " bytes\n"; // 24 bytes
std::cout << "align2: " << sizeof(align2) << " bytes\n"; // 16 bytes
return 0;
}
嵌套的 subStruct
结构是必需的,因为它将在 declared/defined 之外。我正在使用 C++17 和 Visual Studio 2017.
生成的代码可能非常脏或难看。我只是不希望它在以后向我抛出随机错误或在更改配置时中断。
我明确依赖于提出“肮脏或难看”的代码的许可……任何东西。更明确地说,我只是提供一个想法。你需要考验自己,自己承担责任。我认为这个问题明确允许未经测试的代码。
使用此代码:
typedef union
{
struct
{
double d; // 8 bytes
bool b1; //+1 byte (+ 7 bytes padding) = 16 bytes
} nested;
struct
{
double d; // 8 bytes
bool b1, b2; //+2 byte (+ 6 bytes padding) = 16 bytes
} packed;
} t_both;
我希望得到以下结果 attributes/features:
- 包含可能在别处定义类型的子结构(可以从包含的头文件中使用)
- substruct 可访问为
XXX.nested.d
和 XXX.nested.b1
- 地址与
XXX.packed
相同
- 访问
XXX.packed.b2
在 nested
中被认为是填充的内容
- 两个子结构的总大小相同,我希望这意味着即使制作这样的数组也可以
无论您对此做什么,它都可能与以下要求冲突:在写入和读取联合时,所有读取访问必须与最近的写入访问联合的同一部分。因此,写一个读另一个是不被严格允许的。这就是我认为对该代码提案不清楚的地方。也就是说,我经常在相应构造已明确测试的环境中使用这种联合。
为了说明这里是一个功能相同但同样不干净的版本,它更好地说明了子结构可以在别处定义:
/* Inside an included header "whatever.h" : */
typedef struct
{
double d; // 8 bytes
bool b1; //+1 byte (+ 7 bytes padding) = 16 bytes
} t_ExternDefedStruct;
/* Content of including file */
#include "whatever.h"
typedef union
{
t_ExternDefedStruct nested;
struct
{
double d; // 8 bytes
bool b1, b2; //+2 byte (+ 6 bytes padding) = 16 bytes
} packed;
} t_both;
当你的问题是填充时,你的答案是#pragma pack
。
它适用于 MSVC (where it was invented) and also on GCC(添加它是为了与 MSVC 代码库兼容)。
请注意,弄乱对齐可能会非常糟糕。将 multi-byte 成员放在奇数(对他们而言)字节上将导致 run-time 减速。也就是说,在一个很好的情况下,当您的 CPU 完全支持未对齐的操作时。在糟糕的情况下,它会完全崩溃(AFAIK 试图将它们提供给 SSE 指令或非 x86、RISC CPUs)。
我个人知道 #pragma pack(1)
的唯一合法用途是将二进制文件直接映射到结构,尤其是 headers 位图格式,例如 BMP(来自 wingdi.h 的 BITMAPINFOHEADER)或 TGA。另一个是非常大的数据结构,比如@Arty 提到的 gigabyte-sized 数组。
从更大的角度来看,padding 是一个 time-memory trade-off。在绝大多数情况下,访问对齐良好的变量所节省的 CPU 时间非常值得浪费字节。你需要一个很好的理由来改变它,因为如果不认真分析这两种方法,你就不可能取得成功。
使用 #pragma pack(push, 1)
和一些手动填充,您可以使它们相同。
#include <iostream>
int main() {
#pragma pack(push, 1)
struct align1 {
struct {
double d; // 8 bytes
bool b1; //+1 byte (+ 0 bytes padding) = 9 bytes
} subStruct;
bool b2; //+1 byte (+ 0 bytes padding) = 10 bytes
char pad_[6]; //+6 bytes (+ 0 bytes padding) = 16 bytes
};
#pragma pack(pop)
struct align2 {
double d; // 8 bytes
bool b1, b2; //+2 byte (+ 6 bytes padding) = 16 bytes
};
std::cout << "align1: " << sizeof(align1) << " bytes\n"; // 16 bytes
std::cout << "align2: " << sizeof(align2) << " bytes\n"; // 16 bytes
return 0;
}
输出:
align1: 16 bytes
align2: 16 bytes
我在所有流行的编译器中实现了以下用于打包结构的通用宏:
#if defined(_MSC_VER)
#define ATTR_PACKED
#define PACKED_BEGIN __pragma(pack(push, 1))
#define PACKED_END __pragma(pack(pop))
#else
#define ATTR_PACKED __attribute__((packed))
#define PACKED_BEGIN
#define PACKED_END
#endif
在外部结构之前放置PACKED_BEGIN
行,在外部结构之后放置PACKED_END
行,在所有结构(包括内)。所有这些标记的结构将被所有流行的编译器以相同的方式密集打包,打包成尽可能小的尺寸。请参阅下面的代码;您的两个结构都将以相同的方式对齐,大小均为 10 bytes
。
已在在线 C++ 编译器上进行测试(click-open 以下链接可查看测试):MSVC, GCC, and CLang.
如果它被打包得太密集,你可以在需要的地方添加额外的填充字段,在字段之间或字段之后,像 char pad0[2];
和 char pad1[3];
这样的字段来插入 2
和 3
额外的填充字节。
#if defined(_MSC_VER)
#define ATTR_PACKED
#define PACKED_BEGIN __pragma(pack(push, 1))
#define PACKED_END __pragma(pack(pop))
#else
#define ATTR_PACKED __attribute__((packed))
#define PACKED_BEGIN
#define PACKED_END
#endif
#include <iostream>
int main() {
PACKED_BEGIN
struct ATTR_PACKED align1 {
struct ATTR_PACKED {
double d;
bool b1;
} subStruct;
bool b2;
};
PACKED_END
PACKED_BEGIN
struct ATTR_PACKED align2 {
double d;
bool b1, b2;
};
PACKED_END
std::cout << "align1: " << sizeof(align1) << " bytes\n";
std::cout << "align2: " << sizeof(align2) << " bytes\n";
return 0;
}
输出:
align1: 10 bytes
align2: 10 bytes
C++ 11引入了可以使用的关键字“alignas”,这里有一个link(https://en.cppreference.com/w/cpp/language/alignas)
这些结构 align1
和 align2
包含相同的数据,但 align1
由于嵌套布局而具有更多填充。
如何获得 align2
的内存节省对齐方式,同时还使用 align1
中的嵌套结构?
int main() {
struct align1 {
struct {
double d; // 8 bytes
bool b1; //+1 byte (+ 7 bytes padding) = 16 bytes
} subStruct;
bool b2; //+1 byte (+ 7 bytes padding) = 24 bytes
};
struct align2 {
double d; // 8 bytes
bool b1, b2; //+2 byte (+ 6 bytes padding) = 16 bytes
};
std::cout << "align1: " << sizeof(align1) << " bytes\n"; // 24 bytes
std::cout << "align2: " << sizeof(align2) << " bytes\n"; // 16 bytes
return 0;
}
嵌套的 subStruct
结构是必需的,因为它将在 declared/defined 之外。我正在使用 C++17 和 Visual Studio 2017.
生成的代码可能非常脏或难看。我只是不希望它在以后向我抛出随机错误或在更改配置时中断。
我明确依赖于提出“肮脏或难看”的代码的许可……任何东西。更明确地说,我只是提供一个想法。你需要考验自己,自己承担责任。我认为这个问题明确允许未经测试的代码。
使用此代码:
typedef union
{
struct
{
double d; // 8 bytes
bool b1; //+1 byte (+ 7 bytes padding) = 16 bytes
} nested;
struct
{
double d; // 8 bytes
bool b1, b2; //+2 byte (+ 6 bytes padding) = 16 bytes
} packed;
} t_both;
我希望得到以下结果 attributes/features:
- 包含可能在别处定义类型的子结构(可以从包含的头文件中使用)
- substruct 可访问为
XXX.nested.d
和XXX.nested.b1
- 地址与
XXX.packed
相同
- 访问
XXX.packed.b2
在nested
中被认为是填充的内容
- 两个子结构的总大小相同,我希望这意味着即使制作这样的数组也可以
无论您对此做什么,它都可能与以下要求冲突:在写入和读取联合时,所有读取访问必须与最近的写入访问联合的同一部分。因此,写一个读另一个是不被严格允许的。这就是我认为对该代码提案不清楚的地方。也就是说,我经常在相应构造已明确测试的环境中使用这种联合。
为了说明这里是一个功能相同但同样不干净的版本,它更好地说明了子结构可以在别处定义:
/* Inside an included header "whatever.h" : */
typedef struct
{
double d; // 8 bytes
bool b1; //+1 byte (+ 7 bytes padding) = 16 bytes
} t_ExternDefedStruct;
/* Content of including file */
#include "whatever.h"
typedef union
{
t_ExternDefedStruct nested;
struct
{
double d; // 8 bytes
bool b1, b2; //+2 byte (+ 6 bytes padding) = 16 bytes
} packed;
} t_both;
当你的问题是填充时,你的答案是#pragma pack
。
它适用于 MSVC (where it was invented) and also on GCC(添加它是为了与 MSVC 代码库兼容)。
请注意,弄乱对齐可能会非常糟糕。将 multi-byte 成员放在奇数(对他们而言)字节上将导致 run-time 减速。也就是说,在一个很好的情况下,当您的 CPU 完全支持未对齐的操作时。在糟糕的情况下,它会完全崩溃(AFAIK 试图将它们提供给 SSE 指令或非 x86、RISC CPUs)。
我个人知道 #pragma pack(1)
的唯一合法用途是将二进制文件直接映射到结构,尤其是 headers 位图格式,例如 BMP(来自 wingdi.h 的 BITMAPINFOHEADER)或 TGA。另一个是非常大的数据结构,比如@Arty 提到的 gigabyte-sized 数组。
从更大的角度来看,padding 是一个 time-memory trade-off。在绝大多数情况下,访问对齐良好的变量所节省的 CPU 时间非常值得浪费字节。你需要一个很好的理由来改变它,因为如果不认真分析这两种方法,你就不可能取得成功。
使用 #pragma pack(push, 1)
和一些手动填充,您可以使它们相同。
#include <iostream>
int main() {
#pragma pack(push, 1)
struct align1 {
struct {
double d; // 8 bytes
bool b1; //+1 byte (+ 0 bytes padding) = 9 bytes
} subStruct;
bool b2; //+1 byte (+ 0 bytes padding) = 10 bytes
char pad_[6]; //+6 bytes (+ 0 bytes padding) = 16 bytes
};
#pragma pack(pop)
struct align2 {
double d; // 8 bytes
bool b1, b2; //+2 byte (+ 6 bytes padding) = 16 bytes
};
std::cout << "align1: " << sizeof(align1) << " bytes\n"; // 16 bytes
std::cout << "align2: " << sizeof(align2) << " bytes\n"; // 16 bytes
return 0;
}
输出:
align1: 16 bytes align2: 16 bytes
我在所有流行的编译器中实现了以下用于打包结构的通用宏:
#if defined(_MSC_VER)
#define ATTR_PACKED
#define PACKED_BEGIN __pragma(pack(push, 1))
#define PACKED_END __pragma(pack(pop))
#else
#define ATTR_PACKED __attribute__((packed))
#define PACKED_BEGIN
#define PACKED_END
#endif
在外部结构之前放置PACKED_BEGIN
行,在外部结构之后放置PACKED_END
行,在所有结构(包括内)。所有这些标记的结构将被所有流行的编译器以相同的方式密集打包,打包成尽可能小的尺寸。请参阅下面的代码;您的两个结构都将以相同的方式对齐,大小均为 10 bytes
。
已在在线 C++ 编译器上进行测试(click-open 以下链接可查看测试):MSVC, GCC, and CLang.
如果它被打包得太密集,你可以在需要的地方添加额外的填充字段,在字段之间或字段之后,像 char pad0[2];
和 char pad1[3];
这样的字段来插入 2
和 3
额外的填充字节。
#if defined(_MSC_VER)
#define ATTR_PACKED
#define PACKED_BEGIN __pragma(pack(push, 1))
#define PACKED_END __pragma(pack(pop))
#else
#define ATTR_PACKED __attribute__((packed))
#define PACKED_BEGIN
#define PACKED_END
#endif
#include <iostream>
int main() {
PACKED_BEGIN
struct ATTR_PACKED align1 {
struct ATTR_PACKED {
double d;
bool b1;
} subStruct;
bool b2;
};
PACKED_END
PACKED_BEGIN
struct ATTR_PACKED align2 {
double d;
bool b1, b2;
};
PACKED_END
std::cout << "align1: " << sizeof(align1) << " bytes\n";
std::cout << "align2: " << sizeof(align2) << " bytes\n";
return 0;
}
输出:
align1: 10 bytes
align2: 10 bytes
C++ 11引入了可以使用的关键字“alignas”,这里有一个link(https://en.cppreference.com/w/cpp/language/alignas)