C++ calling convention for passing big objects on Linux/x86-84
Asked Answered
I

1

7

I'm trying to understand the overhead of pass object by value as a function parameter in C++/Linux/x86-64 platform.

The experimental code I used for the exploration is posted below and on godbolt.org: https://godbolt.org/z/r9Yfv4

Assume the function is unary. What I observed is:

  1. If the parameter object is a 8 bytes in size, it will be put in RDI.
  2. If the parameter is 16 bytes in size (contain two sub-object each 8 bytes), the two sub-objects will be put in to RDI and RSI.
  3. If the parameter is bigger than 16 bytes, it will be passed via stack.

I only consider integral types and pointers and the composition types of these basic types. I know passing floats/doubles is different.

The size of std::function is 32 bytes (GCC/Linux implementation, long + long + pointer + pointer = 32 bytes.). So passing std::function by value should look like pass struct Person4 defined in my code. But the output assembly shows that pass std::function is very different from pass struct Person3. It looks like std::function is passed via a pointer, am I right? Why there is such a difference?

#include <functional>

struct Person0 {
  long name;
};

long GetName(Person0 p) {
  return p.name;
}

struct Person1 {
  long name;
  long age;
};

long GetName(Person1 p) {
  return p.name;
}

struct Person2 {
  long name;
  long age;
  long height;
};

long GetName(Person2 p) {
  return p.name;
}

struct Person3 {
  long name;
  long age;
  long height;
  long weight;
};

long GetName(Person3 p) {
  return p.name + sizeof(p);
}

long Invoke(std::function<long(long)> f) {
  return f(20) + sizeof(f);
}


int main() {
  Person3 p;
  p.name = 13;
  p.age = 23;
  p.height = 33;
  p.weight = 43;
  long n = GetName(p);

  std::function<long(long)> ff;
  Invoke(ff);
  return 0;
}
Idaline answered 22/1, 2021 at 2:4 Comment(3)
Your description refers to a struct Person4 but there is none in your code.Fike
Could it be std::function's type erasure that makes the compiler do that? (just adding to the confusion perhaps - intersting question)Aspersion
@Fike struct Person4 should be struct Person3, thanks for pointing out this.Idaline
K
4

The document that you want to read is the System V ABI for x86-64, in particular, section 3.2.3 «Paramater Passing»

Structs that are > 32 bytes, go always on the stack. For structs that are <= 32 bytes, there is some logic going on:

Paramater Passing Rules

The post merger cleanup says that given that the size is greater than 2 eighbytes (16 bytes), and the first parameter is not an SSE, or any other parameter is not SSEUP, the whole aggregate is classified as MEMORY (stack).

Regarding the use of std::function, there is one last rule that might explain it:

  1. If a C++ object has either a non-trivial copy constructor or a non-trivial destructor, it is passed by invisible reference (the object is replaced in the parameter list by a pointer that has class INTEGER)
Kendy answered 22/1, 2021 at 2:45 Comment(3)
The TL:DR: on that SSE/SSEUP stuff is that a struct or other object composed of all float / double members (such as __m128) can go in an XMM or YMM reg if its size is right; otherwise it's like any object.Prevaricate
Yep, that last one does it. And indeed if you add a copy constructor or a destructor to Person3 it then behaves the same as std::function.Rainbolt
Thank you very much for the explanation.Idaline

© 2022 - 2024 — McMap. All rights reserved.