2016-12-09 3 views
1

В последнее время я анализировал некоторые части более старого кода, где в некоторых случаях значение, возвращаемое функцией, было присвоено переменной const, а иногда и const&. Из любопытства я переключился на разборку, чтобы увидеть различия. Но прежде, чем добраться до точки, позвольте мне сделать простой пример, чтобы иметь некоторый код для обозначения:Возвращаемое значение const & and const назначение - дизассемблирование

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; 
} 

Следующая разборку выше код был приобретен с VS2015 с оптимизациями отключенными.

Disassembly for <code>(1)``(2)``(3)</code> Я не специалист ASM, но на первый взгляд, я бы сказал, что для (1) и (2) есть подобные операции, выполняемые. Тем не менее, я удивлен тем, что (3) выполняет две дополнительные операции (lea и mov) по сравнению с предыдущими версиями, где const& не использовался при присвоении значения переменной.

То же самое можно наблюдать, когда данные возвращаются из функции по значению. (5) выполняет еще две операции по отношению к (4). Disassembly for <code>(4)``(5)</code>

на вопросы довольно узкий:

  • Где эти дополнительные операции берутся и какова их цель здесь? В общем не так, как здесь: What's the purpose of the LEA instruction, но в представленном контексте.
  • Может ли это повлиять на производительность, по крайней мере, для объектов, для которых размер подстилающих данных ничтожно мал? (в отличие от структуры Data, используемой в примере)
  • Может ли это повлиять на оптимизацию? (Для выпуска сборки)

Кстати, я уже читал Why not always assign return values to const reference? о преимуществах и недостатках использования сопзЬ и сопзИте & при присвоении значения, которые могут быть несколько связанными, но не является частью вопроса.

+1

Ваш компилятор реализованного 'varRef' ссылка как обычный«указатель в маскировке». Инструкция 'lea' вычисляет начальное значение для этого указателя (' ecx = ebp - 84h'), тогда как команда 'mov' сохраняет это значение в указателе' varRef'. – AnT

ответ

4

в случае (3) компилятор создать «скрытый» локальный Var «зЬй :: строка» в [ebp-84] пусть имя его _84 и сделать код, как это

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

X& v - по смыслу и двоичного кода же, как X* v - v фактически указатель на X как в случае, просто разные синтаксис, используемый

то же самое и в случае, если (5)

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

или сказать, если вы вместо того, чтобы выровнять

const Data& dataRef = getData(); 

записи линия

const Data& dataRef = dataVal; 

вы мнение, что эта линия займет ровно 2 ассемблерный:

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

код (4,5) и Data getData() подпись - абсолютный кошмар, нет слов


для большей ясности о возвращении структур «по значению» - функция может возвращать только регистрироваться в качестве результата (al, ax, eax и rax в x64) или 2 регистров - edx:eax (8 байт, EDX в высокой) или rdx:rax (16 байт в x64)

в случае Data getData() - невозможно возвращение Data как есть. как ?!?

так на самом деле ваша функция преобразуется в

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

и код для

//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 

Ьгу известково сколько бессмысленно тетсру делать компилятор?

нужно написать код, как этот

void InitData(Data* p); 

Data dataVal; 
InitData(&dataVal); 
+0

Невозможно делать такие вещи, как 'void InitData (Data * p);' когда вы можете просто включить оптимизацию и позволить компилятору сделать это преобразование для вас, когда это выгодно. Черт, я не смог убедить g ++ не встраивать вызовы 'getData' на что-нибудь выше -O1, и даже тогда RVO пнул и построил объект на своем месте. –

+0

@MilesBudnek - да, современные компиляторы могут серьезно оптимизировать не оптимизированный исходный код. но думаю, что лучший вариант - понять, что мы делаем, понять, что под капотом - и при начале писать лучше код. невозможно вернуть значение более 2-х размеров регистра из функции. действительно нужно передать указатель на данные. и функция заполнить эти данные – RbMm

+1

Лучший код - это код, который легко читать. 'Data dataVal = getData();' четко выражает намерение программиста, и компилятор будет генерировать идентичный объектный код для него или 'InitData (& dataVal);'.Даже на -O0 g ++ генерирует почти точно такой же код для обоих этих выражений. Я согласен с тем, что хорошо знать, что происходит под капотом, но также важно знать, что компилятор действительно хорош в своей работе. –