Анализатор обнаружил функцию, которая принимает параметр по ссылке на константный объект, когда эффективнее это делать по копии.
Рассмотрим два примера для 64-битных систем.
В первом случае функция принимает объекты типа const std::string_view &:
uint32_t foo_reference(const std::string_view &name) noexcept
{
return static_cast<uint32_t>(8 + name.size()) + name[0];
}
Ассемблерный код:
foo_reference(std::basic_string_view<char, std::char_traits<char> > const&):
mov eax, dword ptr [rdi] // <= (1)
mov rcx, qword ptr [rdi + 8] // <= (2)
movsx ecx, byte ptr [rcx]
add eax, ecx
add eax, 8
ret
В нём при каждом чтении данных из объекта типа const std::string_view & происходит разыменование. Это инструкции mov eax, dword ptr [rdi] (1) и mov rcx, qword ptr [rdi + 8] (2).
Во втором — функция принимает объекты типа std::string_view:
uint32_t foo_value(std::string_view name) noexcept
{
return static_cast<uint32_t>(8 + name.size()) + name[0];
}
Ассемблерный код:
foo_value(std::basic_string_view<char, std::char_traits<char> >):
movsx eax, byte ptr [rsi]
add eax, edi
add eax, 8
ret
Компилятор сгенерировал меньше кода для второго примера. Так происходит потому, что объект полностью помещается в регистры процессора и нет необходимости в адресации для доступа к нему.
Примечание N1. Для обратных ситуаций, когда выгоднее передача по ссылке на константу, а объект передаётся по значению, предусмотрено диагностическое правило V813.
Примечание N2. Пользователь может сам указать размер структур, которые выгоднее передавать по копии.
Например, чтобы анализатор выдавал предупреждения только для структур размером не более 8 байт, в исходном коде или в файле конфигурации (.pvsconfig) можно написать комментарий:
//V_CHEAP_TO_COPY_THRESHOLD=8
Значение по умолчанию, при котором анализатор считает, что объект выгоднее передавать по копии, равняется размеру двух указателей на целевой платформе. Например, на большинстве платформ с 64-битной архитектурой размер указателя составляет 8 байт, поэтому на них значение по умолчанию равно 16 байтам.
Вы также можете указать эту настройку только для конкретной платформы. Сделать это можно с помощью секций настроек в файле конфигурации (.pvsconfig).
Например, мы рекомендуем для Windows x64 выставить значение в 8 байт, т.к. это соответствует конвенции вызовов Microsoft x64. Для этого в файл конфигурации (.pvsconfig) нужно добавить следующее содержимое:
//V_SECTION_BEGIN
//V_WHEN_TARGET_OS: Windows
//V_WHEN_TARGET_ARCH: x64
//V_CHEAP_TO_COPY_THRESHOLD=8
//V_SECTION_END
Примечание N3. В некоторых ситуациях передача объектов по ссылке на константу может быть выгоднее передачи по значению, если у класса имеется нетривиальный конструктор копирования.
Например, передача объектов типа std::shared_ptr по значению приводит к созданию дополнительного владельца ресурса и увеличению атомарного счётчика ссылок. В зависимости от требуемого поведения программы такая передача по значению может быть нежелательной.
В реальных проектах могут быть и более сложные случаи. Для подавления таких предупреждений можно воспользоваться одним из способов, описанных в документации.
Правила передачи параметров зависят от используемой архитектуры и ABI. Во многих распространённых 64-битных ABI небольшие объекты размером до 16 байт могут эффективно передаваться через регистры. Поэтому по умолчанию диагностическое правило V835 считает дешёвыми для копирования объекты размером до 16 байт.
Однако это значение не является универсальным. Например, согласно конвенции вызовов Microsoft x64 объекты размером 16 байт передаются косвенно, тогда как в Windows ARM64 объекты такого размера могут передаваться через регистры.
Рассмотрим "System V Application Binary Interface AMD64 Architecture Processor Supplement", регламентирующий соглашения о вызовах функций для Unix-подобных систем. Пункт 3.2.3 описывает передачу параметров. Для каждого из них определяется свой класс. Если параметр имеет класс MEMORY, то он будет передаваться через стек. В противном случае параметр передаётся через регистры процессора, как в приведённом выше примере.
Согласно подпункту 5 (C), если размер объекта превышает 16 байт, ему присваивается класс MEMORY. Исключение составляют агрегатные типы размером до 64 байт, первое поле которых имеет класс SSE, а все остальные — SSEUP. Это означает, что более крупные объекты размещаются на стеке вызова функции, и для доступа к ним также требуется адресация.
Рассмотрим ещё два примера для 64-битных систем.
В третьем — по копии принимается объект размером 16 байт:
struct SixteenBytes
{
int64_t firstHalf; // 8-byte
int64_t secondHalf; // 8-byte
}; // 16-bytes
uint32_t foo_16(SixteenBytes obj) noexcept
{
return obj.firstHalf + obj.secondHalf;
}
Ассемблерный код:
foo_16(SixteenBytes): # @foo_16(SixteenBytes)
lea eax, [rsi + rdi]
ret
Компилятор сгенерировал эффективный код, разместив структуру в двух 64-битных регистрах.
В четвёртом примере по копии принимается структура размером 24 байта:
struct MoreThanSixteenBytes
{
int64_t firstHalf; // 8-byte
int64_t secondHalf; // 8-byte
int32_t yetAnotherStuff; // 4-byte
}; // 24-bytes
uint32_t foo_more_than_16(MoreThanSixteenBytes obj) noexcept
{
return obj.firstHalf + obj.secondHalf + obj.yetAnotherStuff;
}
Ассемблерный код:
foo_more_than_16(MoreThanSixteenBytes):
mov eax, dword ptr [rsp + 16]
add eax, dword ptr [rsp + 8]
add eax, dword ptr [rsp + 24]
ret
Согласно System V ABI компилятор вынужден разместить структуру на стеке. Это приводит к тому, что доступ к ней происходит косвенно — через адрес, который вычисляется с помощью регистра rsp. В таком случае будет выдано предупреждение V813.