Инициализация с помощью списка std::initializer_list включает копирование

Удобным способом инициализации контейнеров STL является использование списка инициализаторов, как показано здесь:

auto data = std::vector<std::string>{"example", "input", "data"};
Вход в полноэкранный режим Выйти из полноэкранного режима

Есть оговорка: аргументы конструктора вектора сначала конструируются, а затем копируются. Это свойство std::initializer_list. Это может стать проблемой, когда параметры не являются тривиально копируемыми, поскольку они обрабатывают некоторые ресурсы и т.д. Короткие строки не являются проблемой, так как они должны обрабатываться оптимизацией для маленьких строк, но длинные (зависит от реализации стандартной библиотеки, какие из них маленькие, а какие нет) потребуют три выделения памяти: одно при создании строки, второе для элемента вектора и третье для копирования.

auto data = std::vector<std::string>{
    "long string that will likely be allocated on the heap"
}; // three memory allocations
Вход в полноэкранный режим Выход из полноэкранного режима

Напишем тестовую программу, которая отслеживает количество выделений памяти:

void* operator new (std::size_t count) {
    std::cout << "new " << count << " bytesn";
    return std::malloc(count);
}

int main() {
    std::cout << "Vector with small string:n";
    auto data = std::vector<std::string>{"small string"};
    std::cout << "nVector with long string:n";
    data = std::vector<std::string>{
        "long string that will likely be allocated on the heap"
    };
}
Вход в полноэкранный режим Выход из полноэкранного режима

Я скомпилировал ее с помощью g++ 9.4.0 с включенными оптимизациями: g++ test.cpp --std=c++17 -O2 -o test. Результат выполнения программы таков:

./test
Vector with small string:
new 32 bytes

Vector with long string:
new 56 bytes
new 32 bytes
new 56 bytes
Вход в полноэкранный режим Выход из полноэкранного режима

Здесь видно, что для небольшой строки std::vector выделил 32 байта, что является размером std::string. Создание вектора для работы с длинной строкой включает в себя 3 выделения памяти: первое происходит при построении параметра, второе — это выделение памяти для std::string внутри вектора и последнее — это копия нашего параметра.

Чтобы избавиться от копии, мы должны избегать std::initializer_list. Давайте вместо этого напишем функцию make_vector. Мой проект выглядит следующим образом:

template <typename T, typename... U>
std::vector<std::decay_t<T>> make_vector(T&& arg, U&&... args) {
    auto result = std::vector<std::decay_t<T>>{};
    result.reserve(1 + sizeof...(args));
    result.emplace_back(std::forward<T>(arg));
    (result.emplace_back(std::forward<U>(args)), ...);
    return result;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь параметры не будут копироваться, потому что они передаются методу emplace_back, который конструирует их на месте. Важно выделить память для всех параметров сразу, используя метод reserve, иначе мы сэкономим выделения на копировании, но добавим лишние выделения для элементов вектора.

Давайте настроим тестовую программу:

int main() {
    std::cout << "String size: " << sizeof(std::string) << "n";
    std::cout << "nVector with small string:n";
    auto data = std::vector<std::string>{"short vector"};
    std::cout << "nVector with a long string:n";
    data = std::vector<std::string>{
        "a long string that will likely be allocated on the heap"};
    using namespace std::literals;
    std::cout << "nmake_vector with a long string:n";
    data = make_vector(
        "a long string that will likely be allocated on the heap"s);
    std::cout << "nVector with two long strings:n";
    data = std::vector<std::string>{
        "a long string that will likely be allocated on the heap",
        "another long string that will likely be allocated on the heap"};
    std::cout << "nmake_vector, two long strings:n";
    data = make_vector(
        "a long string that will likely be allocated on the heap"s,
        "another long string that will likely be allocated on the heap"s);
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь вывод выглядит лучше, я добавил комментарии для пояснения:

String size: 32

Vector with small string:
new 32 bytes # vector item (sizeof std::string)

Vector with a long string:
new 56 bytes # the argument
new 32 bytes # the vector item
new 56 bytes # argument copy

make_vector with a long string:
new 56 bytes # the string argument
new 32 bytes # the vector item

Vector with two long strings:
new 56 bytes # the first argument
new 62 bytes # the second argument
new 64 bytes # memory for two vector items
new 56 bytes # copy of the 1st argument
new 62 bytes # copy of the 2nd argument

make_vector, two long strings:
new 62 bytes # the second argument
new 56 bytes # the first argument
new 64 bytes # memory for two vector items
Вход в полноэкранный режим Выход из полноэкранного режима

Строки больше не копируются при использовании make_vector, что также удобно.

Оцените статью
devanswers.ru
Добавить комментарий