数组的POD结构
POD structure of array
我正试图在 C 编程中围绕 SOA [数组结构] 思考。
我有一些简单的数学函数,我认为它们是相当不错的标量实现。
这里是一个简单的vector 3数据结构
struct vector3_scalar {
float p[3];
};
以及我编写的用于添加这些 vector 3 数据结构中的两个的典型函数。
struct vector3_scalar* vec3_add(struct vector3_scalar* out,
const struct vector3_scalar* a,
const struct vector3_scalar* b) {
out->p[0] = a->p[0] + b->p[0];
out->p[1] = a->p[1] + b->p[1];
out->p[2] = a->p[2] + b->p[2];
return out;
}
我知道这个简单的数据结构没有正确填充,但对于标量,我只想在开始实现其他功能之前先得到一些有用的东西。
现在我的问题是,该结构 'sans padding issues' 是设置数据结构的好方法吗?
这些呢?
struct vector3_scalar {
float p[3];
};
struct vector3_scalar {
float px;
float py;
float pz;
};
或我可以布置数据的任何其他方式。我个人不介意翻转数据结构,因为这个数学的用户不应该在代码编写和优化后变得这么低并弄乱它,只是更高级别的函数,例如;
vec3 *a = vec3_create(0, 1, 0);
vec3 *b = vec3_create(1, 0, 0);
vec3 *c = vec3_zero(void);
vec3* vec3_add(vec3* out, const vec3* in_a, const vec3* in_b);
c = vec3_add(c, a, b); // c == 1, 1, 0
以便您可以内联或单独使用该函数。
vec3 *d = vec3_create(10, 10, 10);
vec3 *e = vec3_create(1, 1, 1);
vec3 *f = vec3_zero();
/* c + d = 11, 11, 10 */ /* c + e = 2, 2, 1 */
vec3_add(f, vec3_add(c, d), vec3_add(c, e));
vec3_free(a);
...
vec3_free(f);
正如您从 public api 中看到的那样,除了实现者之外,底层结构应该并不重要。
我想使用这样的数据布局编写我已经编写的基本标量版本:
struct vector3_scalar {
float p[3];
}
但我愿意改变它,因为它可以工作并且看起来足够稳定,符合我的口味。
为什么不两者兼而有之?
#include <stdio.h>
struct {
union {
float as_arr[3];
struct {
float px;
float py;
float pz;
};
};
} my_point;
int main (void)
{
my_point.as_arr[1] = .5;
printf ("%f\n", my_point.py);
return 0;
}
您似乎无法理解填充的实际含义。这只是编译器自动完成的优化。
由于某些hardware/architectural原因,一次从内存中获取了多个字节。比方说 4,这对于今天的架构来说是很常见的。此外,您不能从内存中获取任何 4 个连续字节。第一个字节的地址必须能被4整除。
现在,假定以下结构:
struct x {
char a;
int b;
};
假设我们有一个地址为 100 的 x 实例。因此,在地址 100 的字节中,您将存储 a,并且每当您对 a 进行操作时,您都会获取字节 100、101、102、103。
现在让我们来看看 b 会发生什么。如果 b 将在 a 之后立即开始(即在地址 101 处),则需要 2 次获取。首先,获取字节 100、101、102、103,然后获取 104、105、106、107,然后进行一些计算以获取存储在字节 101-104 中的整数。为了避免这种情况,编译器会在 a 之后插入 3 个字节的填充,这将使您的结构占用 8 个字节,而不是您期望的 5 个字节。但请注意,这只是性能优化(基本上是用内存 space 来换取执行时间),除非您有很多实例或做一些非常高级的事情,否则这对您来说无关紧要。
此外,对于您的情况,从内存的角度来看,结构的声明是等效的。
这两个都是 AoS 表示,而不是 SoAs:
struct vector3_scalar {
float p[3];
};
struct vector3_scalar {
float px;
float py;
float pz;
};
它们是完全相同的东西,并且对齐和填充相同。例如,在我的上,sizeof(vector3_scalar) == 12
(3 个浮点数的大小)。
如果我们谈论 AoS 向量表示,前者可能有利于与 C API 通信,例如,想要接受一个指向浮点数的指针,并且还可以通过允许您循环访问来帮助避免冗余代码矢量分量。
SoA 看起来像这样:
struct vector4
{
float x[4];
float y[4];
float z[4];
};
它将每个字段表示为一个单独的数组,对于 SIMD,通常大小为 4 的倍数。
在这种情况下,sizeof(vector4) == 48/4 == 12
。我们在这里没有任何内存节省的好处,因为 vector3_scalar
没有填充。尽管如此,SoA 代表更有利于希望使用 128 位寄存器并受益于对齐内存的 SSE。在那种情况下,我们必须将第 4 个组件引入 vector3_scalar
才能直接将其加载到 XMM 寄存器中,从而使单个向量占用 16 个字节而不是 12 个字节。此外,取决于你在做什么,AoS 代表可能需要更多 horizontal/shuffle 类型的操作,而 SIMD 并不总是那么快,而 SoA 代表可以一次非常简单地向量化一种组件类型的操作。
但是让我们举个例子:
struct Person
{
int y;
char x;
};
在这种情况下,我的 sizeof(Person) == 8
这意味着添加了 3 个字节的填充以进行对齐。如果我们使用 SoA 代表:
struct Persons
{
int y[4];
char x[4];
};
...sizeof(Persons) == 20
。 20 / 4 == 5
。所以现在我们每个人有 5 个字节而不是每个人 8 个字节,同时每个字段仍然正确对齐。
至于从性能的角度来看这是否有益,而不仅仅是从节省内存的角度来看,这在很大程度上取决于哪些字段是 hot/cold、人数等。您也可以使用 AoSoA :
struct Persons
{
struct Person4
{
int y[4];
char x[4];
};
Person4 persons_x4[n/4];
};
这往往会很好地平衡事情,因为随着 n
变得越来越大,它不会线性降低同一个人字段之间的空间局部性。
我正试图在 C 编程中围绕 SOA [数组结构] 思考。
我有一些简单的数学函数,我认为它们是相当不错的标量实现。
这里是一个简单的vector 3数据结构
struct vector3_scalar {
float p[3];
};
以及我编写的用于添加这些 vector 3 数据结构中的两个的典型函数。
struct vector3_scalar* vec3_add(struct vector3_scalar* out,
const struct vector3_scalar* a,
const struct vector3_scalar* b) {
out->p[0] = a->p[0] + b->p[0];
out->p[1] = a->p[1] + b->p[1];
out->p[2] = a->p[2] + b->p[2];
return out;
}
我知道这个简单的数据结构没有正确填充,但对于标量,我只想在开始实现其他功能之前先得到一些有用的东西。
现在我的问题是,该结构 'sans padding issues' 是设置数据结构的好方法吗?
这些呢?
struct vector3_scalar {
float p[3];
};
struct vector3_scalar {
float px;
float py;
float pz;
};
或我可以布置数据的任何其他方式。我个人不介意翻转数据结构,因为这个数学的用户不应该在代码编写和优化后变得这么低并弄乱它,只是更高级别的函数,例如;
vec3 *a = vec3_create(0, 1, 0);
vec3 *b = vec3_create(1, 0, 0);
vec3 *c = vec3_zero(void);
vec3* vec3_add(vec3* out, const vec3* in_a, const vec3* in_b);
c = vec3_add(c, a, b); // c == 1, 1, 0
以便您可以内联或单独使用该函数。
vec3 *d = vec3_create(10, 10, 10);
vec3 *e = vec3_create(1, 1, 1);
vec3 *f = vec3_zero();
/* c + d = 11, 11, 10 */ /* c + e = 2, 2, 1 */
vec3_add(f, vec3_add(c, d), vec3_add(c, e));
vec3_free(a);
...
vec3_free(f);
正如您从 public api 中看到的那样,除了实现者之外,底层结构应该并不重要。
我想使用这样的数据布局编写我已经编写的基本标量版本:
struct vector3_scalar {
float p[3];
}
但我愿意改变它,因为它可以工作并且看起来足够稳定,符合我的口味。
为什么不两者兼而有之?
#include <stdio.h>
struct {
union {
float as_arr[3];
struct {
float px;
float py;
float pz;
};
};
} my_point;
int main (void)
{
my_point.as_arr[1] = .5;
printf ("%f\n", my_point.py);
return 0;
}
您似乎无法理解填充的实际含义。这只是编译器自动完成的优化。
由于某些hardware/architectural原因,一次从内存中获取了多个字节。比方说 4,这对于今天的架构来说是很常见的。此外,您不能从内存中获取任何 4 个连续字节。第一个字节的地址必须能被4整除。
现在,假定以下结构:
struct x {
char a;
int b;
};
假设我们有一个地址为 100 的 x 实例。因此,在地址 100 的字节中,您将存储 a,并且每当您对 a 进行操作时,您都会获取字节 100、101、102、103。
现在让我们来看看 b 会发生什么。如果 b 将在 a 之后立即开始(即在地址 101 处),则需要 2 次获取。首先,获取字节 100、101、102、103,然后获取 104、105、106、107,然后进行一些计算以获取存储在字节 101-104 中的整数。为了避免这种情况,编译器会在 a 之后插入 3 个字节的填充,这将使您的结构占用 8 个字节,而不是您期望的 5 个字节。但请注意,这只是性能优化(基本上是用内存 space 来换取执行时间),除非您有很多实例或做一些非常高级的事情,否则这对您来说无关紧要。
此外,对于您的情况,从内存的角度来看,结构的声明是等效的。
这两个都是 AoS 表示,而不是 SoAs:
struct vector3_scalar {
float p[3];
};
struct vector3_scalar {
float px;
float py;
float pz;
};
它们是完全相同的东西,并且对齐和填充相同。例如,在我的上,sizeof(vector3_scalar) == 12
(3 个浮点数的大小)。
如果我们谈论 AoS 向量表示,前者可能有利于与 C API 通信,例如,想要接受一个指向浮点数的指针,并且还可以通过允许您循环访问来帮助避免冗余代码矢量分量。
SoA 看起来像这样:
struct vector4
{
float x[4];
float y[4];
float z[4];
};
它将每个字段表示为一个单独的数组,对于 SIMD,通常大小为 4 的倍数。
在这种情况下,sizeof(vector4) == 48/4 == 12
。我们在这里没有任何内存节省的好处,因为 vector3_scalar
没有填充。尽管如此,SoA 代表更有利于希望使用 128 位寄存器并受益于对齐内存的 SSE。在那种情况下,我们必须将第 4 个组件引入 vector3_scalar
才能直接将其加载到 XMM 寄存器中,从而使单个向量占用 16 个字节而不是 12 个字节。此外,取决于你在做什么,AoS 代表可能需要更多 horizontal/shuffle 类型的操作,而 SIMD 并不总是那么快,而 SoA 代表可以一次非常简单地向量化一种组件类型的操作。
但是让我们举个例子:
struct Person
{
int y;
char x;
};
在这种情况下,我的 sizeof(Person) == 8
这意味着添加了 3 个字节的填充以进行对齐。如果我们使用 SoA 代表:
struct Persons
{
int y[4];
char x[4];
};
...sizeof(Persons) == 20
。 20 / 4 == 5
。所以现在我们每个人有 5 个字节而不是每个人 8 个字节,同时每个字段仍然正确对齐。
至于从性能的角度来看这是否有益,而不仅仅是从节省内存的角度来看,这在很大程度上取决于哪些字段是 hot/cold、人数等。您也可以使用 AoSoA :
struct Persons
{
struct Person4
{
int y[4];
char x[4];
};
Person4 persons_x4[n/4];
};
这往往会很好地平衡事情,因为随着 n
变得越来越大,它不会线性降低同一个人字段之间的空间局部性。