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

时间:2019-03-18 01:14:43

标签: c

我是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. 哪一种是最优化的形式。据我了解,它是add_pointers,因为没有任何内容被复制。但是,然后您就无法执行简单的add(1, 1),因此从API的角度来看这没什么好玩的。

3 个答案:

答案 0 :(得分:1)

每次调用带有参数的函数时,您都将复制参数的值。在您的示例中,只需要复制指针值还是整数值即可。复制int不会比复制指针快或慢,但是使用指针时,只要取消引用指针,您就可以从内存中另外读取一次。

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

答案 1 :(得分:1)

  

这意味着在性能方面比传递指针要慢

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

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

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

总体而言,对于初学者来说,不要过多地考虑性能,这是一个高级主题,并取决于特定的系统。取而代之的是,重点放在编写尽可能可读的代码上。

答案 2 :(得分:1)

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

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

因为编译器可能会基本上完全避免子例程的调用/返回,并且将在每种使用情况下使用最佳扩展(在大多数情况下,这将作为单个add r3, r8指令内联。)在当今许多多核和流水线CPU中,占用的时间远远少于单个时钟周期。

如果您无权使用此类编译器,则模拟该方案的最佳方法可能是:

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

,您将在保持函数符号的同时进行内联。但是,前提是您要求功能,前提是前提会失败,前提是前提是前提:)

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

当然,如果编译器知道该函数是可缓存的,并且在同一块的一个表达式中以相同的参数多次使用它,则可以将结果值缓存为稍后使用,并节省再次进行总和的费用。事情似乎就好像最好的处理方法是查看我们正在处理的确切方案。但是,到目前为止,将两个数相加的主要成本是将其放入函数信封的成本。

编辑

我尝试了以下示例,并将其编译为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

无论如何,您都会看到对函数的调用,因为没有通知编译器手头有什么类型的函数(即使不会使用结果代码作为函数)会产生副作用),无论如何都必须调用它。

顺便提一句,如前所述,将引用传递给函数的方式涉及取消引用那些指针,这通常意味着内存访问(这比将两个寄存器加在一起要昂贵得多)