Имеет ли передача `unique_ptr` по значению снижение производительности по сравнению с простым указателем? - PullRequest
0 голосов
/ 17 января 2019

Общепринято, что std::unique_ptr не приводит к снижению производительности ( и не к потере памяти, если не используется параметр удаления ), но недавно я наткнулся на обсуждение, показывающее что он фактически вводит дополнительную косвенность, потому что unique_ptr нельзя передать в регистр на платформах с Itanium ABI. Опубликованный пример был похож на

#include <memory>

int foo(std::unique_ptr<int> u) {
    return *u;
}

int boo(int* i) {
    return *i;
}

, который генерирует дополнительную инструкцию ассемблера в foo по сравнению с boo.

foo(std::unique_ptr<int, std::default_delete<int> >):
        mov     rax, QWORD PTR [rdi]
        mov     eax, DWORD PTR [rax]
        ret
boo(int*):
        mov     eax, DWORD PTR [rdi]
        ret

Объяснение состояло в том, что Itanium ABI требует, чтобы unique_ptr не передавался в регистр из-за нетривиального конструктора, поэтому он создается в стеке, а затем адрес этого объекта передается в регистре.

Я знаю, что это не сильно влияет на производительность на современной платформе ПК, но мне интересно, может ли кто-нибудь предоставить более подробную информацию о причинах, по которым он не будет скопирован в реестр. Поскольку абстракции с нулевой стоимостью являются одной из основных целей C ++, мне интересно, обсуждалось ли это в процессе стандартизации как допустимое отклонение или это вопрос качества реализации. Потеря производительности, безусловно, достаточно мала, если учесть преимущества, особенно на современных платформах ПК.

Комментаторы отметили, что эти две функции не полностью эквивалентны, и поэтому сравнение некорректно, поскольку foo также вызовет средство удаления для параметра unique_ptr, но boo не освобождает память. Однако меня интересовала только разница, возникающая при передаче значения unique_ptr по сравнению с передачей простого указателя. Я изменил пример кода и включил вызов delete, чтобы освободить простой указатель ; вызов находится в вызывающей стороне, поскольку средство удаления unique_ptr также вызывается в контексте вызывающей стороны, чтобы сделать сгенерированный код более идентичным. Кроме того, руководство delete также проверяет ptr != nullptr, потому что деструктор также делает это. Тем не менее, foo не передает параметр в регистр и должен сделать косвенный доступ.

Мне также интересно, почему компилятор не отменяет проверку для nullptr перед вызовом operator delete, так как в любом случае это определено как noop. Я думаю, что unique_ptr может быть специализированным для средства удаления по умолчанию, чтобы он не выполнял проверку в деструкторе, но это было бы очень маленькой микрооптимизацией.

1 Ответ

0 голосов
/ 17 января 2019

System V ABI использует Itanium C ++ ABI и ссылается на него. В частности, C ++ Itanium ABI указывает, что

Если тип параметра нетривиален для целей вызовов, вызывающий должен выделить место для временного и передать этот временный ссылка.

В частности:

...

Если тип имеет нетривиальный деструктор, вызывающая сторона вызывает этот деструктор после того, как элемент управления возвращается к нему (в том числе, когда вызывающая сторона выдает исключение) в конце заключающего полное выражение.

Итак, простой ответ на вопрос ", почему он не передан в регистр ", это ", потому что он не может ".

Теперь интересным вопросом может быть ', почему C ++ Itanium ABI решил пойти с этим '.

Хотя я бы не утверждал, что обладаю глубокими знаниями с обоснованием, на ум приходят две вещи:

  • Это позволяет удалить копию, если аргумент функции является временным
  • Это делает оптимизацию хвостового вызова более мощной. Если вызываемый объект должен будет вызывать деструкторы своих аргументов, TCO будет невозможен для любой функции, которая принимает нетривиальные аргументы.
...