1

Lately I've been analyzing some parts of an older code where in some cases value returned from function was assigned to const variable and sometimes to const&. Out of curiousity I've switched to the dissasembly to see the differences. But before getting to the point let me draw a simple example to have some code to refer to:

struct Data
{
    int chunk[1024];
};

Data getData()
{
    return Data();
}

int main()
{
    const std::string varInit{ "abc" }; // 1
    const std::string varVal = "abc"; // 2
    const std::string& varRef = "abc"; // 3

    const Data dataVal = getData(); // 4
    const Data& dataRef = getData(); // 5

    return 0;
}

The following disassembly of the above code was acquired with VS2015 with optimizations disabled.

Disassembly for <code>(1)``(2)``(3)</code> I'm no asm specialist but at first glance I'd say that for (1) and (2) there are similar operations performed. Nevertheless, I'm surprised that (3) carries two additional operations (lea and mov) comparing to previous versions where the const& was not used during variable value assignment.

Same can be observed when the data is returned from a function by value. (5) carries two more operations in relation to (4). Disassembly for <code>(4)``(5)</code>

The questions are quite narrow:

  • Where do these additional operations come from and what is their purpose here? Not in general like here: What's the purpose of the LEA instruction but in the presented context.
  • Can this influence the performance at least for objects for which the underlaying data size is negligible? (in contrast to Data struct used in the example)
  • Would this have any impact when the optimization is turned on? (for release builds)

By the way, I've already read Why not always assign return values to const reference? about pros and cons of using const and const& when assigning values which can be somewhat related but is not a part of the question.

Community
  • 1
  • 1
Dusteh
  • 1,496
  • 16
  • 21
  • 1
    Your compiler implemented `varRef` reference as an ordinary "pointer in disguise". The `lea` instruction calculates the initial value for that pointer (`ecx = ebp - 84h`), while `mov` instruction saves that value into the `varRef` pointer. – AnT stands with Russia Dec 10 '16 at 00:54

1 Answers1

4

in case (3) compiler create 'hidden' local var 'std::string' at [ebp-84] let name it _84 and do code like this

const std::string _84 = "abc"; 
const std::string& varRef = _84;// lea + move (by sense varRef = &_84)

the X& v - by sense and binary code same as X* v - v is actually pointer to X in both case, simply different syntax used

the same and in case (5)

const Data _20a0 = getData(); 
const Data& dataRef = _20a0; // by sense dataRef = &_20a0, but by syntax dataRef = _20a0

or say if you instead line

const Data& dataRef = getData();

write line

const Data& dataRef = dataVal;

you view that this line take exactly 2 asm instructions:

lea eax,[dataVal]
mov [dataRef],eax

code (4,5) and Data getData() signature is absolute nightmare, no words


for more clarity about return structs 'by value' - function can return only register as result (al, ax, eax and rax in x64) or 2 registers - edx:eax (8 byte, edx in high) or rdx:rax (16 byte in x64)

in case Data getData() - impossible return Data as is. how ?!?

so really your function is transformed to

Data* getData(Data* p)
{
    Data x;
    memcpy(p, &x, sizeof(Data));
    return p;
}

and code to

//const Data dataVal = getData(); 
Data _41A0, _3198, dataVal;
memcpy(&_3198, getData(&_41A0), sizeof(Data));
memcpy(&dataVal, &_3198, sizeof(Data));

//const Data& dataRef = getData(); 
Data _41A0, _3198, _20a0, &dataRef;
memcpy(&_51a8, getData(&_61b0), sizeof(Data));
memcpy(&_20a0, &_51a8, sizeof(Data));
dataRef = &_20a0;// very small influence compare all other

try calc how many senseless memcpy do compiler ?

need write code like this

void InitData(Data* p);

Data dataVal;
InitData(&dataVal);
RbMm
  • 31,280
  • 3
  • 35
  • 56
  • There's really no reason to do things like `void InitData(Data* p);` when you could just turn on optimization and let the compiler do that transformation for you when it's advantageous. Heck, I was unable to convince g++ not to inline calls to `getData` on anything above -O1, and even then RVO kicked in and constructed the object in place. – Miles Budnek Dec 10 '16 at 01:13
  • @MilesBudnek - yes, modern compilers can serious optimize not optimized source code. but think that the best option - understand what are we doing, understand what under the hood - and at begin write better code. impossible return value more than 2 register size from function. really need pass pointer to data. and function fill this data – RbMm Dec 10 '16 at 01:20
  • 1
    The best code is code that is easy to read. `Data dataVal = getData();` clearly expresses the programmer's intent and the compiler will generate identical object code for it or `InitData(&dataVal);`. Even on -O0 g++ generates almost exactly the same code for both of those expressions. I agree that it's good to know what's going on under the hood, but it's also important to know that the compiler is really good at its job. – Miles Budnek Dec 10 '16 at 01:38
  • Thanks, nice 'behind the scenes' explanation. I've checked the `const Data& dataRefFromVal = dataVal;` which actually takes only the 2 mentioned instructions - this is was what I was missing. Seems to me that in general using `const&` while variable assignment, in the era of RVO and copy elision, makes mostly sense when you assign to an object whose lifetime is limited to the current scope. For example iterating though a container like `vector dataVect=getDataVect(); for(const auto& item: dataVect){}`. Just to be safe, optimization will 'handle the rest'. – Dusteh Dec 10 '16 at 23:16