在C中模拟可变大小的结构;分配,性能问题

时间:2015-04-01 15:00:14

标签: c memory-management struct memory-alignment

可以将具有自定义长度的数组放在C中的结构中的任何位置,但在这种情况下,需要额外的malloc次调用。有些编译器允许在结构中的任何地方使用VLA,但这不符合标准。所以我决定在struct中为标准C模拟VLA。

我处于一种我必须获得最大性能的情况。 C中的代码将自动生成,因此在这种情况下可读性或样式并不重要。

在静态大小成员之间将有许多自定义大小数组成员的结构。以下是这种结构的一种非常简单的形式。

struct old_a {
    int n_refs;
    void **refs;
    int count;
};

struct old_a *old_a_new(int n_refs, int count) {
    struct old_a *p_a = malloc(sizeof(struct old_a));
    p_a->n_refs = n_refs;
    p_a->refs = malloc(n_refs * sizeof(void *));
    p_a->count = count;
    return p_a;
}

#define old_a_delete(p_a) do {\
    free(p_a->refs);\
    free(p_a);\
} while (0)

可以避免对malloc进行额外的refs调用,如下所示。

#define a_get_n_refs(p_a) *(int *)p_a
#define a_set_n_refs(p_a, rval) *(int *)p_a = rval
#define a_get_count(p_a) *(int *)((char *)p_a + sizeof(int) + a_get_n_refs(p_a) * sizeof(void *))
#define a_set_count(p_a, rval) *(int *)((char *)p_a + sizeof(int) + a_get_n_refs(p_a) * sizeof(void *)) = rval
#define a_get_refs(p_a, i) *(void **)((char *)p_a + sizeof(int) + i * sizeof(void *))
#define a_set_refs(p_a, i, rval) *(void **)((char *)p_a + sizeof(int) + i * sizeof(void *)) = rval

static void *a_new(int n_refs, int count) {
    void *p_a = malloc(sizeof(int) + n_refs * sizeof(void *) + sizeof(int));
    a_set_n_refs(p_a, n_refs);
    a_set_count(p_a, count);
    return p_a;
}

#define a_delete(p_a) do {\
    free(p_a);\
} while (0)

模拟版本在我的机器上运行速度比使用指针阵列快12~14%。我认为这是由于对mallocfree的调用次数增加了一半,并且解除引用次数减少了。测试代码如下。

int main(int argc, char **argv) {
    const int n_as = atoi(argv[1]) * 10000;
    const int n_refs = n_as;
    const int count = 1;
    unsigned int old_sum = 0;
    unsigned int sum = 0;
    clock_t timer;

    timer = clock();
    struct old_a **old_as = malloc(n_as * sizeof(struct old_a));
    for (int i = 0; i < n_as; ++i) {
        old_as[i] = old_a_new(n_refs, count);
        for (int j = 0; j < n_refs; ++j) {
            old_as[i]->refs[j] = (void *)j;
            old_sum += (int)old_as[i]->refs[j];
        }
        old_sum += old_as[i]->n_refs + old_as[i]->count;
        old_a_delete(old_as[i]);
    }
    free(old_as);
    timer = clock() - timer;
    printf("old_sum = %u; elapsed time = %.3f\n", old_sum, (double)timer / CLOCKS_PER_SEC);

    timer = clock();
    void **as = malloc(n_as * sizeof(void *));
    for (int i = 0; i < n_as; ++i) {
        as[i] = a_new(n_refs, count);
        for (int j = 0; j < n_refs; ++j) {
            a_set_refs(as[i], j, (void *)j);
            sum += (int)a_get_refs(as[i], j);
        }
        sum += a_get_n_refs(as[i]) + a_get_count(as[i]);
        a_delete(as[i]);
    }
    free(as);
    timer = clock() - timer;
    printf("sum = %u; elapsed time = %.2f\n", sum, (double)timer / CLOCKS_PER_SEC);
    return 0;
}

使用gcc test.c -otest -std=c99编译:

>test 4
old_sum = 3293684800; elapsed time = 7.04
sum = 3293684800; elapsed time = 6.07

>test 5
old_sum = 885958608; elapsed time = 10.74
sum = 885958608; elapsed time = 9.44

如果我的代码有任何未定义的行为,实现定义的行为等,请告诉我。对于具有理智(标准兼容)C编译器的机器,它意味着100%便携。

我知道内存对齐问题。这些模拟结构的成员只有intdoublevoid *,所以我认为不存在对齐问题,但我不确定。虽然模拟结构可以在我的机器(Windows 7 64位,MinGW / gcc)中运行得更快,但我不知道它是如何与其他硬件或编译器一起运行的。除了检查标准的保证行为外,我真的需要有关硬件知识的帮助;哪一个是机器友好的代码(最好是一般的)?

3 个答案:

答案 0 :(得分:0)

有一点需要注意 - 在某些系统上,int将是2个字节而不是4个。在这种情况下,int将只达到32767.由于您将输入乘以10000,它几乎肯定会在这些机器上引起问题。请改用long。

答案 1 :(得分:0)

除非您的程序中有相当大比例的工作要分配和释放这些数据结构,否则您在分配/解除分配速度中观察到的差异不太可能对程序的总体执行时间产生显着影响

此外,请注意这两种方法并不相同。后者不会生成struct old_a的表示,因此使用所生成的数据结构的任何其他代码都必须使用提供的访问宏(或等效的)来执行此操作。

此外,roll-your-own-struct方法存在潜在的对齐问题。根据与实现相关的大小和各种类型的对齐要求,可能导致伪结构内的指针数组的成员未对齐。如果确实如此,则会导致速度损失甚至程序崩溃。

更一般地说,关于类型表示的大小几乎没有安全的假设。假设int的大小与void *的大小相同,或者任何一个大小与{double相同,肯定是 un 安全。 1}}。

答案 2 :(得分:-2)

  

可以将具有自定义长度的数组放在C中的结构中的任何位置,但在这种情况下需要额外的malloc调用

不,不是

有着名的&#34; struct hack&#34;获取一次性分配数组的结构

struct name {
    int namelen;
    char namestr[1];
};

然后

struct name *makename(char *newname)
{
    struct name *ret = malloc(sizeof(struct name)-1 + strlen(newname)+1);
            /* -1 for initial [1]; +1 for \0 */
    if(ret != NULL) {
        ret->namelen = strlen(newname);
        strcpy(ret->namestr, newname);
    }

    return ret;
}

有关详细信息,请参阅http://c-faq.com/struct/structhack.html

更新

如上所述,数组是结构的最后一个成员,可能是不合理的限制

更新

在C99中,这是现在有福的方式,称为flexible array member,声明应被修改为

struct name {
    int namelen;
    char namestr[];
};

然后它将工作并一次性分配