C中的值或结构指针多参数

时间:2018-07-24 11:02:57

标签: c pointers struct stack micro-optimization

让我们假设这个结构:

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;
}

我的第一个假设是传递值,因为值只会复制堆栈中的所有内容,并且没有任何好处,但是为每个字段重新引用对象是否比复制它更昂贵?指针版本是否可能不会以大量的缓存丢失而堆栈版本不会结束?

2 个答案:

答案 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请求更多的内存,他已经拥有了他所需要的(指针和值)。

您唯一需要关注的是...不要丢失您的指针,并且在更新任何数据时始终知道要更改什么。