我试图在c编程中围绕SOA [数组结构]。
我有一些简单的数学函数,我写的是我认为相当不错的标量实现。
这里是一个简单的向量3数据结构
struct vector3_scalar {
float p[3];
};
和我编写的一个典型函数,用于添加其中两个向量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问题'是设置数据结构的好方法吗?
这些呢?
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);
从公共API可以看出,除了实现者之外,底层结构应该不重要。
我想编写基本的标量版本,我已经使用这样的数据布局编写了这个版本:
struct vector3_scalar {
float p[3];
}
但我愿意改变它,因为它的工作原理似乎足够稳定,符合我的口味。
答案 0 :(得分:1)
这两个都是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代表可能需要更多的水平/随机类型操作,这些操作对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个字节,同时仍然让每个字段正确对齐。
至于从性能角度来看这是否有益,而不仅仅是从节省内存的角度来看,它在很大程度上取决于哪些领域是热/冷,人数等等。您还可以使用AoSoA:
struct Persons
{
struct Person4
{
int y[4];
char x[4];
};
Person4 persons_x4[n/4];
};
这往往可以很好地平衡事情,因为它不会使同一个人的字段之间线性地降低空间位置,因为n
变得越来越大。
答案 1 :(得分:0)
为什么不两者?
#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;
}
答案 2 :(得分:0)
您似乎无法理解填充实际上是什么。它只是编译器自动完成的优化。
由于某些硬件/架构原因,一次从内存中提取多个字节。让我们说4,这对于今天的架构来说非常普遍。此外,您无法连续4个字节获取表单内存。第一个字节的地址必须可以被4整除。
现在,假设以下结构:
struct x {
char a;
int b;
};
假设我们有一个x的实例,其地址为100.因此,在地址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个字节。但请注意,这只是一个性能优化(基本上是执行时间的交易内存空间),除非你有很多实例或做一些非常高级的事情,否则对你来说并不重要。
此外,在您的情况下,从内存的角度来看,结构的声明是等效的。