我最近在越来越多的项目中使用C,几乎最终创建了自己的带有结构指针的“对象实现”。但是,我很好奇纯粹的功能样式(带结构)和结构之间的速度差异,这些结构在更现代的面向对象风格中调用函数指针。
我已经创建了一个示例程序,并且不确定为什么时间差异如此之大。
该程序使用两个计时器并记录完成每项任务所需的时间(一个接一个)。这不包括内存分配/解除分配,并且两种技术都以类似的方式设置(每个结构都有三个整数作为结构的指针)。
代码本身只是在宏 LOOP_LEN 中指定的持续时间内在for循环中重复添加三个数字。
请注意我测量的函数内联,编译器优化从无到完全优化(/ 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;
}
每次运行程序时,函数编程总是执行得更快。我能想到的唯一原因是它必须通过结构访问额外的指针层来调用方法,但我认为内联会减少这种延迟。
虽然延迟随着优化的增加而变小,但我很想知道为什么它在低/无优化级别上有这么大的不同,因此这被认为是一种有效的编程风格?
答案 0 :(得分:8)
带有/O2
循环的第二个循环被编译为:
call clock
mov edi, eax ; this is used later to calculate time
call clock
E.g。 根本没有代码。编译器能够理解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
将sum
和sum_str
编译为等效的指令序列。
区别在于循环中的一条指令,而间接调用则较慢。总的来说,没有优化的两个版本之间应该没有太大的区别 - 两者都应该很慢。
答案 1 :(得分:1)
我想伊万和你已经提供了答案。我只想添加内联函数。即使您将函数声明为内联,编译器也不一定总是将其视为内联函数。基于复杂性编译器可能会将其视为正常函数。
答案 2 :(得分:0)
正如你所说,前一种情况有额外的指针引用间接。虽然您将sum
声明为内联函数,但由于sum
函数指针被放入对象成员中,因此无法轻松内联。
我建议您将生成的汇编代码与-O0
〜-O3
进行比较。