Search code examples
cgopointersconventions

Pass a pointer instead of return new variable in C and Go?


Why is it convention in C and Go to pass a pointer to a variable and change it rather return a new variable with the value?

In C:

#include <stdio.h>

int getValueUsingReturn() {
    int value = 42;
    return value;
}

void getValueUsingPointer(int* value ) {
    *value = 42;
}

int main(void) {
  int valueUsingReturn = getValueUsingReturn();
  printf("%d\n", valueUsingReturn);

  int valueUsingPointer;
  getValueUsingPointer(&valueUsingPointer);
  printf("%d\n", valueUsingPointer);
  return 0;
}

In Go:

package main

import "fmt"

func getValueUsingReturn() int {
    value := 42
    return value
}

func getValueUsingPointer(value *int) {
    *value = 42
}

func main() {
    valueUsingReturn := getValueUsingReturn()
    fmt.Printf("%d\n", valueUsingReturn)

    var valueUsingPointer int
    getValueUsingPointer(&valueUsingPointer)
    fmt.Printf("%d\n", valueUsingPointer)
}

It there any performance benefits or restrictions in doing one or the other?


Solution

  • First off, I don't know enough about Go to give a judgement on it, but the answer will apply in the case of C.

    If you're just working on primitive types like ints, then I'd say there is no performance difference between the two techniques.

    When structs come into play, there is a very slight advantage of modifying a variable via pointer (based purely on what you're doing in your code)

    #include <stdio.h>
    
    struct Person {
        int age;
        const char *name;
        const char *address;
        const char *occupation;
    };
    
    struct Person getReturnedPerson() {
        struct Person thePerson = {26, "Chad", "123 Someplace St.", "Software Engineer"};
        return thePerson;
    }
    
    void changeExistingPerson(struct Person *thePerson) {
        thePerson->age = 26;
        thePerson->name = "Chad";
        thePerson->address = "123 Someplace St.";
        thePerson->occupation = "Software Engineer";
    }
    
    int main(void) {
      struct Person someGuy = getReturnedPerson();
      
    
      struct Person theSameDude;
      changeExistingPerson(&theSameDude);
      
      
      return 0;
    }
    

    GCC x86-64 11.2

    With No Optimizations

    Returning a struct variable through the function's return is slower because the variable has to be "built" by assigning the desired values, after which, the variable is copied to the return value.

    When you're modifying a variable by pointer indirection, there is nothing to do except write the desired values to the memory addresses (based off the pointer you passed in)

    .LC0:
            .string "Chad"
    .LC1:
            .string "123 Someplace St."
    .LC2:
            .string "Software Engineer"
    getReturnedPerson:
            push    rbp
            mov     rbp, rsp
            mov     QWORD PTR [rbp-40], rdi
            mov     DWORD PTR [rbp-32], 26
            mov     QWORD PTR [rbp-24], OFFSET FLAT:.LC0
            mov     QWORD PTR [rbp-16], OFFSET FLAT:.LC1
            mov     QWORD PTR [rbp-8], OFFSET FLAT:.LC2
            mov     rcx, QWORD PTR [rbp-40]
            mov     rax, QWORD PTR [rbp-32]
            mov     rdx, QWORD PTR [rbp-24]
            mov     QWORD PTR [rcx], rax
            mov     QWORD PTR [rcx+8], rdx
            mov     rax, QWORD PTR [rbp-16]
            mov     rdx, QWORD PTR [rbp-8]
            mov     QWORD PTR [rcx+16], rax
            mov     QWORD PTR [rcx+24], rdx
            mov     rax, QWORD PTR [rbp-40]
            pop     rbp
            ret
    changeExistingPerson:
            push    rbp
            mov     rbp, rsp
            mov     QWORD PTR [rbp-8], rdi
            mov     rax, QWORD PTR [rbp-8]
            mov     DWORD PTR [rax], 26
            mov     rax, QWORD PTR [rbp-8]
            mov     QWORD PTR [rax+8], OFFSET FLAT:.LC0
            mov     rax, QWORD PTR [rbp-8]
            mov     QWORD PTR [rax+16], OFFSET FLAT:.LC1
            mov     rax, QWORD PTR [rbp-8]
            mov     QWORD PTR [rax+24], OFFSET FLAT:.LC2
            nop
            pop     rbp
            ret
    main:
            push    rbp
            mov     rbp, rsp
            sub     rsp, 64
            lea     rax, [rbp-32]
            mov     rdi, rax
            mov     eax, 0
            call    getReturnedPerson
            lea     rax, [rbp-64]
            mov     rdi, rax
            call    changeExistingPerson
            mov     eax, 0
            leave
            ret
    

    With Slight Optimization

    However, most compilers today can figure out what you're trying to do here, and will equalize the performance between the two techniques.

    If you want to be absolutely stingy, passing pointers is still slightly faster by a few clock cycles at best.

    In returning a variable from the function, you still have to at least set the address of the return value.

            mov     rax, rdi
    

    But in passing the pointer, not even this is done.

    But other than that, the two techniques have no performance difference.

    .LC0:
            .string "Chad"
    .LC1:
            .string "123 Someplace St."
    .LC2:
            .string "Software Engineer"
    getReturnedPerson:
            mov     rax, rdi
            mov     DWORD PTR [rdi], 26
            mov     QWORD PTR [rdi+8], OFFSET FLAT:.LC0
            mov     QWORD PTR [rdi+16], OFFSET FLAT:.LC1
            mov     QWORD PTR [rdi+24], OFFSET FLAT:.LC2
            ret
    changeExistingPerson:
            mov     DWORD PTR [rdi], 26
            mov     QWORD PTR [rdi+8], OFFSET FLAT:.LC0
            mov     QWORD PTR [rdi+16], OFFSET FLAT:.LC1
            mov     QWORD PTR [rdi+24], OFFSET FLAT:.LC2
            ret
    main:
            mov     eax, 0
            ret