0

Consider this code:

typedef struct my_struct { int a; int b; int c; } my_struct;

void some_function(my_struct *value)
{
    // do something
}

int main(void)
{
    for (;;) {
        some_function(&(my_struct){1, 2, 3});
    }

    return 0;
}

I plan to always set a, b and c to 1, 2 and 3. Therefore, the object passed to some_function will always be the same. In this case, does the compiler re-create and re-destroy the object at each iteration? Or does it convert it to something similar to this:

typedef struct my_struct { int a; int b; int c; } my_struct;

void some_function(my_struct *value)
{
    // do something
}

int main(void)
{
    my_struct once_and_for_all = {1, 2, 3};

    for (;;) {
        some_function(&once_and_for_all);
    }

    return 0;
}

Which doesnt do any memory allocation/deallocation in the loop, and runs at optimal speed.

I prefer the first syntax as it is less verbose, but I am also worried about performance.

I use GCC.

12
  • 2
    Depends? The function accepts by a non-const pointer, so a compiler may choose to assume it modifies the structure, in which case it must recreate it (even in the same storage). Commented Sep 12, 2024 at 10:15
  • 2
    If your function is inlined, you should see no performance difference (using GCC/Clang). If the function is not inlined, the second is typically faster. That being said, pushing a 64-bit pointer (on 64-bit arch) vs 3 x 32-bit integers is not a huge difference. In this case, the function inlining matters more than the copy. Commented Sep 12, 2024 at 10:31
  • 1
    @StoryTeller-UnslanderMonica: The fact the function accepts a non-const pointer is irrelevant to whether the compiler may assume the function modifies the structure, because it is defined behavior in the C standard to take a const pointer and use the result to modify the object it points to, as long as that object was not originally defined with const. So both a compiler must assume both the function f(const T *) and the function f(T *) may modify the pointed-to data, unless it can see their definitions or some extension informs it about the nature of the function. Commented Sep 12, 2024 at 11:31
  • 1
    @StoryTeller-UnslanderMonica: It is not relevant. As defined by the standard, both a function declared f(T *p) and f(const T *p) may be used to modify the memory pointed to by p. Per the standard, the const qualifier on a pointed-to type serves only advisory role: The implementation must issue a diagnostic if code attempts to modify a const-qualified lvalue. But it is permitted to convert the pointer to T * and use * (T *) p as a non-const-qualified lvalue, provided the object was not originally defined as const. The compiler must assume f(const T *p) may change *p. Commented Sep 12, 2024 at 11:47
  • 1
    @StoryTeller-UnslanderMonica: “Relevant” is not about the importance of what assumptions the compiler makes but about the logic of what assumptions the compiler makes. To conform to the C standard, a compiler may not assume a function passed a pointer parameter p does not modify memory pointed to by p (in the absence of any other information, such as seeing the function definition), and whether the parameter is declared T * or const T * does not alter that. You were not only engaging with OP when you wrote your comment; every user may read it, and the statement misinforms them. Commented Sep 12, 2024 at 11:55

2 Answers 2

4

In this case, does the compiler re-create and re-destroy the object at each iteration?

Semantically, yes. A compound literal appearing at block scope has automatic storage duration associated with the innermost containing block. Its lifetime ends when execution of that block terminates for any reason.

But that doesn't put many constraints on the under-the-hood behavior of the implementation.

You seem to be concerned about the cost of allocation and deallocation inside the loop, but allocation and deallocation of automatic objects is typically very fast. Often effectively free. On a stack-based machine, the memory used, if any, will be on the stack, and the same location will ordinarily be re-used on every loop iteration.

The main difference between the two alternatives you present is that semantically, the former requires the object to be initialized on every loop iteration, whereas the latter must initialize it only once. Only if the compiler determines that the literal is not modified within the body of the loop, including indirectly by some_function(), can it consider lifting the initialization out of the loop to treat your first variation as if it were the second.

If you want to encourage the compiler to make that assumption then you could declare some_function as ...

void some_function(const my_struct *value);

... and define the literal as ...

(const my_struct) { /* ...  */ }

. If for some reason that's not semantically viable for you, then the optimization you're hoping for is inappropriate in the first place.

I prefer the first syntax as it is less verbose, but I am also worried about performance.

Such a concern is premature. Any performance difference between your two variations is likely to be too small too measure. Even if your structure were very large, so that you might actually be able to observe the cost of initializing it, you don't know whether that cost -- if even incurred -- is large enough to justify micro-optimization without measuring the program's performance and profiling it to see where it's spending its time.

In general, write clean and clear code, using appropriate, efficient algorithms. Use available language features to express your intent in as much detail as you can. Enable compiler optimizations, and then don't worry about performance until you identify an actual performance problem.

Sign up to request clarification or add additional context in comments.

2 Comments

As observed here godbolt.org/z/v7W8qfjsT, changing the function and compound literal to const does not change the assembler output. Which is strange - I can't come up with any other explanation than a missed optimization opportunity in both gcc and clang. (C23 constexpr doesn't improve anything either)
@john "Enable compiler optimizations, and then don't worry about performance until you identify an actual performance problem." --> so true.
3

Disassemble it and see for yourself. Just declare the function with external linkage first and leave the function definition out of the code so that the compiler can't assume anything about it:

void some_function (my_struct *value);

Then gcc -O3 for x86 gives this:

main:
        push    rbp
        push    rbx
        sub     rsp, 24
        mov     rbp, QWORD PTR .LC0[rip]
        mov     rbx, rsp
.L2:
        mov     rdi, rbx
        mov     QWORD PTR [rsp], rbp
        mov     DWORD PTR [rsp+8], 3
        call    some_function
        jmp     .L2
.LC0:
        .long   1
        .long   2

That is: stack space is reserved once but the values are copied into that space at each lap of the loop. Since the function doesn't have a const qualified parameter, the compiler can't assume that the values aren't modified by the function.

However, changing the parameter to const my_struct *value didn't improve the code. It seems like a missed optimization, since I can't think of anything in the C standard which is forcing the compiler to update the literal over and over when the function parameter is const. clang behaves the same.

Using your second version of the code fixes the problem. Then instead we get:

.L2:
        mov     rdi, rbx
        call    some_function
        jmp     .L2

5 Comments

The fact the function’s parameter is not const qualified is irrelevant to whether the compiler may assume the function does not modify the structure, because it is defined behavior in the C standard to take a const pointer and use the result to modify the object it points to, as long as that object was not originally defined with const. So both a compiler must assume both the function f(const T *) and the function f(T *) may modify the pointed-to data, unless it can see their definitions or some extension informs it about the nature of the function.
@EricPostpischil I know that but there is no way to access the object in this case except through the const pointer and "casting away const" would be undefined behavior. Furthermore, declaring both the parameter and the compound literal const doesn't change the generated assembly either. godbolt.org/z/v7W8qfjsT So this is a missed optimization, period.
No, casting away const is not undefined behavior. If a pointer started as T * and was converted to const T * (as in automatic conversion when calling a function), then converting it back to T * is defined by C 2018 6.3.2.3 7, “… Otherwise, when converted back again, the result shall compare equal to the original pointer…”, and by the absence of anything in the C standard that says you cannot do this. This is well known and is famously necessary in some uses of strchr and some other standard library routines.
@EricPostpischil Regardless of that, changing the effective type of the compound literal to const qualified does not change the assembly code, as seen in the posted link. In which case pointer aliasing does not matter since "casting away const" would be UB as per 6.7.3 "If an attempt is made to modify an object defined with a const-qualified type through use of an lvalue with non-const-qualified type, the behavior is undefined." Therefore the behavior cannot be explained by referring to allowed forms of pointer conversion/aliasing.
Yes, the compiler seems to be missing an optimization when the compound literal is defined as const. That aside, the sentence “Since the function doesn't have a const qualified parameter, the compiler can't assume that the values aren't modified by the function” is incorrect; the const qualification is not the cause of why the compiler cannot make that assumption. C programmers should not be made to think that passing an argument to a parameter of type const T * will necessarily protect it. The sentence after it should also be updated.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.