如何正确编写一个在C中添加两个整数的函数

问题描述 投票:2回答:3

我是C的新手,感觉最优化的是什么,处理指针,值,引用等的正确方法是什么。

我已经开始创建一个简单的整数add函数。

int
add(int a, int b) {
  return a + b;
}

void
main() {
  // these work
  int sum = add(1, 1);
  int a = 1;
  int b = 1;
  int c = add(a, b);

  // this doesn't
  int d = add(&a, &b);
  int e = add(*a, *b);
}

根据我的理解,做add(a, b)会将值复制到函数中,这意味着它比传入指针的性能更慢。所以我尝试创建两个添加函数,将其重命名为add_values

int
add_pointers(int *a, int *b) {
  return (*a) + (*b);
}

int
add_values(int a, int b) {
  return a + b;
}

void
main() {
  // these work
  int sum = add_values(1, 1);
  int a = 1;
  int b = 1;
  int c = add_values(a, b);

  // this works now
  int d = add_pointers(&a, &b);

  // not sure
  int e = add(*a, *b);
}

我想知道一些事情:

  1. 如果有办法将这两个函数的行为放在一个函数中。或者如果不是(或者在这种情况下性能不是最佳的),如果它只是2个具有命名约定的单独函数,那么通常如何处理这两种情况,等等。
  2. 哪一个是最优化的形式。根据我的理解,它是add_pointers,因为没有任何东西被复制。但是你不能做一个简单的add(1, 1),所以从API的角度来看并不好玩。
c
3个回答
1
投票

这意味着它比传入指针更慢的性能

那就是你弄错了。在典型的32位计算机上,int是32位,指针是32位。因此,两个版本之间的实际数据传递量是相同的。但是,使用指针可以归结为间接访问机器代码,因此在某些情况下实际上可能会产生效率较低的代码。在一般情况下,int add(int a, int b)可能是最有效的。

根据经验,可以通过值将所有标准整数和浮点类型传递给函数。但结构或联合应该通过指针传递。

在这种特殊情况下,编译器可能会“内联”整个函数,将其替换为机器代码中的单个加法指令。之后整个参数传递变为非问题。

总的来说,作为初学者,不要过多考虑性能,这是一个高级主题,取决于具体的系统。相反,专注于尽可能地编写可读代码。


1
投票

无论何时使用参数调用函数,都要复制参数的值。在您的示例中,只是您要复制指针值还是整数值。复制int不会比复制指针明显更快或更慢,但是使用指针时,只要取消引用指针,就可以从内存中读取额外的内容。

对于任何简单的数据类型,您最好只按值接受参数。传递指针更有意义的唯一一次是你正在处理一个可以任意大的数组或struct


1
投票

从我的角度来看,根据你是否有能够使inline函数扩展的编译器,最简单的方法是:

inline int add(int a, int b)
{
    return a + b;
}

因为编译器可能会主要避免子程序调用/返回,并且将使用每个用例中可用的最佳扩展(在大多数情况下,这将作为单个add r3, r8指令内联。)这可能比单个时钟少得多今天许多循环多核和流水线cpu。

如果您无法访问此类编译器,那么模拟该场景的最佳方法可能是:

#define add(a,b) ((a) + (b))

并且你将在保持函数符号的同时进行内联。但是当你要求一个函数时,这个前提失败了,这取决于前提的优先级:)

当您考虑进行函数调用的最佳方法时,首先要考虑的是,对于小函数,您所做的最重的任务是完成一个子程序调用...因为需要时间来推送返回地址,认为是在这种情况下,最糟糕的部分是必须调用一个函数,只是添加它的两个参数(添加两个存储在寄存器中的值只需要一条指令,但是通过函数调用,它至少需要三个---调用,添加和返回)如果加数已经在寄存器中,则添加不需要太多时间,但子程序调用通常需要在堆栈中推送寄存器并稍后弹出。这是两次内存访问,即使缓存在指令缓存中也会花费更多。

当然,如果编译器知道该函数是可缓存的,并且您在同一个块的一个表达式中使用相同的参数多次使用它,它可以缓存稍后要使用的结果值,并节省制作该函数的成本。再说一遍。事情变得好像最好的处理方式是看看我们正在处理的确切场景。但是在这一点上,增加两个数字的主要成本是迄今为止在一个功能范围内监禁它的成本。

EDIT

我尝试了以下示例,并在arm7架构(raspberry pi B +,freebsd,clang编译器)中编译它,结果远远不错:)

inline.c

inline int sum(int a, int b)
{
    return a + b;
}

int main()
{
    int a = 3, b = 2, c = sum(a, b);
}

导致:

    /* ... */
    .file   "inline.c"
    .globl  main                    @ -- Begin function main
    .p2align        2
    .type   main,%function
    .code   32                      @ @main
main:
    .fnstart
@ %bb.0:
    mov     r0, #0
    bx      lr
.Lfunc_end0:
    .size   main, .Lfunc_end0-main

如您所见,main的唯一代码包括在0中存储r0返回值(退出代码);)

以防我将add编译为外部库函数:

int add(int a, int b);

int main()
{
    int a = 3, b = 2, c = sum(a, b);
}

会导致:

    .file   "inline.c"
    .globl  main                    @ -- Begin function main
    .p2align        2
    .type   main,%function
    .code   32                      @ @main
main:
    .fnstart
@ %bb.0:
    .save   {r11, lr}
    push    {r11, lr}
    .setfp  r11, sp
    mov     r11, sp
    mov     r0, #3
    mov     r1, #2
    bl      sum                 <--- the call to the function.
    mov     r0, #0
    pop     {r11, pc}
.Lfunc_end0:
    .size   main, .Lfunc_end0-main
    .cantunwind
    .fnend

你可以看到,无论如何都会调用函数,因为编译器还没有被告知它手头的函数类型(即使结果代码不会被使用,因为函数可以有横向无论如何,必须被召唤。

顺便提一下,如上所述,传递对函数的引用的方法涉及解除引用这些指针,这通常意味着内存访问(这比将两个寄存器一起添加要昂贵得多)

© www.soinside.com 2019 - 2024. All rights reserved.