【问题标题】:Why are structure pointers (methods) in C considerably slower than normal functions?为什么 C 中的结构指针(方法)比普通函数慢得多?
【发布时间】:2018-02-06 16:02:58
【问题描述】:

我最近在越来越多的项目中使用 C,几乎最终用结构指针创建了我自己的“对象实现”。但是,我很好奇纯函数式风格(带有结构)和以更现代的面向对象风格调用函数指针的结构之间的速度差异。


我创建了一个示例程序,但不确定为什么时间差异如此之大。

程序使用两个计时器并记录完成每个任务所花费的时间(一个接一个)。这不包括内存分配/解除分配,两种技术的设置方式相似(每个结构都有三个整数作为结构的指针)。

代码本身只是在一个 for 循环中重复地将三个数字相加,持续时间在宏 LOOP_LEN 中指定。

请注意,我同时测量了 内联 和编译器优化从无到 完全优化 (/Ox) 的函数(我在 Visual Studio 中运行它作为纯 .c 文件)。


对象样式代码

// MAGIC object 
typedef struct {

    // Properties
    int* x;
    int* y;
    int* z;

    // Methods
    void(*init)(struct magic* self, int x, int y, int z);
    int(*sum)(struct magic* self);

}magic;

// Variable init function
void* init(magic* self, int x, int y, int z) {

    // Assign variables to properties
    *self->x = x;
    *self->y = y;
    *self->z = y;

    return;

}

// Add all variables together
inline int sum(magic* self) {
    return ((*self->x) + (*self->y) + (*self->z));
}

// Magic object constructor
magic* new_m(int x, int y, int z) {

    // Allocate self
    magic* self = malloc(sizeof(magic));

    // Allocate member pointers
    self->x = malloc(sizeof(int));
    self->y = malloc(sizeof(int));
    self->z = malloc(sizeof(int));

    // Allocate method pointers
    self->init = init;
    self->sum = sum;

    // Return instance
    return self;
}

// Destructor
void delete_m(magic* self) {

    // Deallocate memory from constructor
    free(self->x); self->x = NULL;
    free(self->y); self->y = NULL;
    free(self->z); self->z = NULL;
    free(self); self = NULL;

    return;

}

功能(传统)样式代码

// None object oriented approach
typedef struct {
    int* x;
    int* y;
    int* z;
}str_magic;

// Magic struct constructor
str_magic* new_m_str(int x, int y, int z) {

    // Allocate self
    str_magic* self = malloc(sizeof(str_magic));

    // Allocate member pointers
    self->x = malloc(sizeof(int));
    self->y = malloc(sizeof(int));
    self->z = malloc(sizeof(int));

    // Return instance
    return self;
}

// Destructor
void delete_m_str(str_magic* self) {

    // Deallocate memory from constructor
    free(self->x); self->x = NULL;
    free(self->y); self->y = NULL;
    free(self->z); self->z = NULL;
    free(self); self = NULL;

    return;

}

// Sum using normal structure type
inline int sum_str(str_magic* self) {
    return ((*self->x) + (*self->y) + (*self->z));
}

定时器测试和主程序入口点

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define LOOP_LEN 1000000000

// Main entry point
int main(void) {

    // Start timer for first task
    clock_t start1, end1, start2, end2;
    double cpu_time_used1, cpu_time_used2;

    // Init instances before timer
    magic* object1 = new_m(1, 2, 3);

    // Start task1 clock
    start1 = clock();

    for (int i = 0; i < LOOP_LEN; i++) {
        // Perform method sum and store result
        int result1 = object1->sum(object1);
    }

    // Stop task1 clock
    end1 = clock();

    // Remove from memory
    delete_m(object1);

    // Calculate task1 execution time
    cpu_time_used1 = ((double)(end1 - start1)) / CLOCKS_PER_SEC;

    // Init instances before timer
    str_magic* object2 = new_m_str(1, 2, 3);

    // Start task2 clock
    start2 = clock();

    for (int i = 0; i < LOOP_LEN; i++) {
        // Perform function and store result
        int result2 = sum_str(object2);
    }

    // Stop task2 clock
    end2 = clock();

    // Remove from memory
    delete_m_str(object2);

    // Calculate task 2 execution time
    cpu_time_used2 = ((double)(end2 - start2)) / CLOCKS_PER_SEC;

    // Print time results
    printf("----------------------\n    Task 1 : %.*e\n----------------------\n    Task 2 : %.*e\n----------------------\n", cpu_time_used1, cpu_time_used2);

    if (cpu_time_used1 < cpu_time_used2) {
        printf("Object Oriented Approach was faster by %.*e\n", cpu_time_used2-cpu_time_used1);
    }
    else {
        printf("Functional Oriented Approach was faster by %.*e\n", cpu_time_used1 - cpu_time_used2);
    }

    // Wait for keyboard interrupt
    getchar();

    return 0;
}

每次运行程序时,函数式编程总是执行得更快。我能想到的唯一原因是它必须通过结构访问额外的指针层才能调用方法,但我认为内联会减少这种延迟。

虽然随着优化的增加延迟会变小,但我很想知道为什么在低/无优化级别会有如此大的不同,因此这是否被认为是一种有效的编程风格?

【问题讨论】:

  • “都内联”是什么意思? MSVC 无法内联您的 OO 代码。同时,它可以通过在第二种情况下完全不生成任何代码来击败您的基准。
  • 您可以尝试在关闭优化器的情况下对其进行编译,看看您是否会在性能上得到类似的差异?
  • self = NULL; 是多余的并且可能是一个错误,如果程序员现在认为释放的项目指向 NULL。它没有。
  • 不确定它是否相关,但这看起来很像比较 C++ 等语言中的虚拟和非虚拟方法调用。您可以通过搜索“虚拟方法”和“性能”找到相关资料。
  • 无论如何,您的代码没有副作用,所以我希望两个循环在这两种情况下都会产生一个大胖子NOP。投票结束,因为无法复制,因为这种基准测试没有意义。现在您需要做的是将示例结果复制到分配的数组中,然后在基准测试之外打印或 volatile 访问这些数组的一些随机点。

标签: c pointers struct


【解决方案1】:

带有/O2 循环的第二个循环编译为:

    call     clock
    mov      edi, eax ; this is used later to calculate time
    call     clock

例如根本没有代码。编译器能够理解 sum_str 函数的结果是未使用的,所以它完全删除它。对于第一种情况,编译器无法做到这一点。

所以启用优化时没有真正的比较。

如果没有优化,只会执行更多代码。

第一个循环编译为:

    cmp      DWORD PTR i$1[rsp], 1000000000
    jge      SHORT $LN3@main                 ; loop exit
    mov      rcx, QWORD PTR object1$[rsp]
    mov      rax, QWORD PTR object1$[rsp]    ; extra instruction
    call     QWORD PTR [rax+32]              ; indirect call
    mov      DWORD PTR result1$3[rsp], eax
    jmp      SHORT $LN2@main                 ; jump to the next iteration

第二次循环:

    cmp      DWORD PTR i$2[rsp], 1000000000
    jge      SHORT $LN6@main                 ; loop exit
    mov      rcx, QWORD PTR object2$[rsp]
    call     sum_str
    mov      DWORD PTR result2$4[rsp], eax
    jmp      SHORT $LN5@main                 ; jump to the next iteration

sumsum_str 都被编译为等效的指令序列。

不同之处在于循环中的一条指令,而且间接调用更慢。总体而言,没有优化的两个版本之间不应该有太大的差异 - 两者都应该很慢。

【讨论】:

    【解决方案2】:

    我想伊万和你已经提供了答案。我只想补充一下内联函数。即使您将函数声明为内联,编译器也不必始终将其视为内联。基于复杂性编译器可能会将其视为正常功能。

    【讨论】:

      【解决方案3】:

      正如你所说,前一种情况有额外的指针引用间接。尽管您将sum 声明为内联函数,但由于sum 函数指针已放入对象成员中,因此不能轻易内联。

      我建议你将生成的汇编代码与-O0 ~ -O3进行比较。

      【讨论】:

        猜你喜欢
        • 2014-02-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-02-18
        • 1970-01-01
        • 2010-10-02
        • 2019-07-02
        • 1970-01-01
        相关资源
        最近更新 更多