让我们假设这个结构:
typedef struct mytest_t {
uint8_t field1;
uint32_t field2;
uint64_t field3;
uint64_t field4;
uint16_t field5;
uint32_t field6;
} mytest_t;
还有一些想要创建此结构的函数(有点像一个对象):
int something_with(uint8_t field1, uint32_t field2, uint64_t field3, uint16_t field5) {
mytest_t *object = malloc(sizeof(mytest_t));
object->field1 = field1;
object->field2 = field2;
object->field3 = field3;
object->field4 = 0x12345678;
object->field5 = field5;
object->field6 = 42;
dosomethingwith(object);
return 0;
}
void initial() {
something_with(123, 456, 789, 456);
}
这些功能纯粹是出于我的情况。此功能就像一个帮助程序,在代码中只有一个点,即对象被填充然后转发到其他对象。
注意:此示例很小,假设参数要长2到3倍。
为避免将大量参数传递给该函数,并使调用变得冗长且难以阅读,我在考虑将预先填充的mytest_t
结构作为参数传递(假设需要的字段正确填充)。
将结构作为值或指针传递会更好吗?取消引用所有字段的成本是多少?既然所有内容都在堆栈中,那有什么区别吗?编译器可以某种方式对其进行优化吗?
void initial() {
mytest_t source = {
.field1 = 123,
.field2 = 456,
.field3 = 789,
// field4 not needed
.field5 = 456,
// field6 not needed
};
call_by_value(source);
call_by_ptr(&source);
}
int call_by_value(mytest_t origin) {
mytest_t *object = malloc(sizeof(mytest_t));
object->field1 = origin.field1;
object->field2 = origin.field2;
object->field3 = origin.field3;
object->field4 = 0x12345678;
object->field5 = origin.field5;
object->field6 = 42;
dosomethingwith(object);
return 0;
}
int call_by_ptr(mytest_t *origin) {
mytest_t *object = malloc(sizeof(mytest_t));
object->field1 = origin->field1;
object->field2 = origin->field2;
object->field3 = origin->field3;
object->field4 = 0x12345678;
object->field5 = origin->field5;
object->field6 = 42;
dosomethingwith(object);
return 0;
}
我的第一个假设是传递值,因为值只会复制堆栈中的所有内容,并且没有任何好处,但是为每个字段重新引用对象是否比复制它更昂贵?指针版本是否可能不会以大量的缓存丢失而堆栈版本不会结束?
答案 0 :(得分:2)
听起来,您的结构成员数量将超过可以在参数中传递的参数数量(至少在我遇到的任何体系结构调用约定中);导致剩余值以任何方式放置在堆栈上。这与将结构按值完全传递到堆栈上没有太大区别。与仅在堆栈上分配它然后再传递一个指针相比,这可能导致更多的复制。
这将为您提供一些选择,具体取决于将如何使用您的结构。例如:
Alex F提到了经验法则 sizeof(type) > 2*sizeof(void*)
(许多调用约定允许将那些“小结构”传递到寄存器中)可以在寄存器中传递。
如果您需要支持多种体系结构并且您的结构超过几个成员(请参见上面的经验法则),则指向struct的指针是最好的简单选择。
如果可以轻松地将结构拆分为常用成员和不常见成员,则可以将其拆分为冷热结构或具有少量热成员和冷结构(或结构)包含冷成员。 (请参见下面的示例)
我提到“冷”结构可能是结构联合的可能性。如果许多成员是多余的,这取决于您的一个“热门”成员,这将很有用(例如,请参见html / dom解析器)。如果这样可以使您的更多结构适合缓存行,则可以从中受益更好的缓存位置和减少的内存占用(也许-取决于结构的实例数-是仅仅是上下文结构还是它们的数组或链接列表?)。除非分析表明它是瓶颈或过多使用资源的原因,否则我不一定建议添加这种复杂性。
struct mystruct {
struct hot{ long node_type; struct mystruct *next;} hot;
struct cold{ /* the rest of your members */ } cold;
}
void myfunc(struct hot x, struct cold *y);
//or
struct mystruct {
long node_type; //hot
struct mystruct *next; //hot
struct cold{ /* the rest of your members*/ } cold;
}
void myfunc0(struct mystruct *next_node, long node_type, struct cold *y);
void myfunc1(long node_type, struct cold *y);
另一方面,如所示,您的结构将有很多无用的填充(如果“打包”,则要求拆包效率低下)。如果从大到小订购您的成员,打包效果会更好。除非该结构的大小超过高速缓存行的大小(这些天通常为〜64字节),否则如果将较小的成员与通常使用的较大的成员分开,则几乎没有影响。有关更多信息,请参见The Lost Art of Structure Packing。
编辑:
一旦按大小对结构进行排序,则可能是您的成员可以方便地放入SIMD寄存器中,甚至可能有助于SIMD操作。 Arm的typedef似乎与标准命名约定一致,并且在支持vector extensions的编译器上使用起来很方便。
这是用于128位的arm向量的相对便携式版本
typedef __INT64_TYPE__ int64x2_t __attribute__ ((__vector_size__ (16), __may_alias__));
typedef __UINT64_TYPE__ uint64x2_t __attribute__ ((__vector_size__ (16), __may_alias__));
typedef __INT32_TYPE__ int32x4_t __attribute__ ((__vector_size__ (16), __may_alias__));
typedef __UINT32_TYPE__ uint32x4_t __attribute__ ((__vector_size__ (16), __may_alias__));
typedef __INT16_TYPE__ int16x8_t __attribute__ ((__vector_size__ (16), __may_alias__));
typedef __UINT16_TYPE__ uint16x8_t __attribute__ ((__vector_size__ (16), __may_alias__));
typedef __INT8_TYPE__ int8x16_t __attribute__ ((__vector_size__ (16), __may_alias__));
typedef __UINT8_TYPE__ uint8x16_t __attribute__ ((__vector_size__ (16), __may_alias__));
typedef double float64x2_t __attribute__ ((__vector_size__ (16), __may_alias__));
typedef float float32x4_t __attribute__ ((__vector_size__ (16), __may_alias__));
///similar for vector sizes 32 and 64
作为x86_64的示例,您可以在整数寄存器中传递(u)int64_t,并使用8个SIMD寄存器传递128个字符类型,64个短字符,32个int或16个长字符的附加组合(对于AVX2或AVX512甚至更多)支持)。这并不是说如果数据不利于SIMD操作,它将更快。这取决于使用方式。
答案 1 :(得分:0)
这取决于,如果您要始终使用相同的值,那么预填充的mytest_t结构将是更好的选择...但是,如果您认为要在另一个函数中的某个位置更改值,然后...将Poiner传递给预先填充的mytest_t更好。
当您通过地址(指针)时,因为编译器(和OS)更好,因为他不需要向OS请求更多的内存,他已经拥有了他所需要的(指针和值)。
您唯一需要关注的是...不要丢失您的指针,并且在更新任何数据时始终知道要更改什么。