Подтвердить что ты не робот

С++ для VLA C99 (цель: сохранить производительность)

Я переношу некоторый код C99, который сильно использует массивы переменной длины (VLA) для С++.

Я заменил VLAs (распределение стека) классом массива, который выделяет память в куче. Снижение производительности было огромным, замедление в 3,2 раза (см. Ниже контрольные показатели). Какую быструю замену VLA можно использовать в С++? Моя цель - свести к минимуму удар производительности при перезаписи кода для С++.

Одна из идей, которые мне предложили, заключалась в том, чтобы написать класс массива, который содержит хранилище фиксированного размера в классе (то есть может быть выделено в стек) и использует его для небольших массивов и автоматически переключается на распределение кучи для больших массивов, Моя реализация этого в конце поста. Он работает достаточно хорошо, но я до сих пор не могу достичь производительности исходного кода C99. Чтобы приблизиться к этому, я должен увеличить это хранилище фиксированного размера (MSL ниже) до размеров, которые мне не нравятся. Я не хочу выделять слишком большие массивы в стеке даже для множества небольших массивов, которые не нужны, потому что я беспокоюсь, что это вызовет переполнение стека. VLA C99 на самом деле менее подвержен этому, потому что он никогда не будет использовать больше хранилища, чем необходимо.

Я столкнулся с std::dynarray, но я понимаю, что он не был принят в стандарт (еще?).

Я знаю, что clang и gcc поддерживают VLA на С++, но мне тоже нужно работать с MSVC. На самом деле лучшая переносимость является одной из основных целей переписывания как С++ (другая цель - сделать программу, которая изначально была инструментом командной строки, в библиотеку многократного использования).


Benchmark

MSL относится к размеру массива, выше которого я переключаюсь на распределение кучи. Я использую разные значения для 1D и 2D массивов.

Оригинальный код C99: 115 секунд.
MSL = 0 (т.е. Распределение кучи): 367 секунд (3.2x).
1D-MSL = 50, 2D-MSL = 1000: 187 секунд (1,63x).
1D-MSL = 200, 2D-MSL = 4000: 143 секунды (1,24x).
1D-MSL = 1000, 2D-MSL = 20000: 131 (1,14x).

Увеличение MSL еще больше повышает производительность, но в итоге программа начнет возвращать неправильные результаты (я предполагаю из-за).

Эти ориентиры с clang 3.7 на OS X, но gcc 5 показывает очень похожие результаты.


Код

Это текущая реализация "smallvector", которую я использую. Мне нужны 1D и 2D векторы. Я переключаюсь на распределение кучи выше размера MSL.

template<typename T, size_t MSL=50>
class lad_vector {
    const size_t len;
    T sdata[MSL];
    T *data;
public:
    explicit lad_vector(size_t len_) : len(len_) {
        if (len <= MSL)
            data = &sdata[0];
        else
            data = new T[len];
    }

    ~lad_vector() {
        if (len > MSL)
            delete [] data;
    }

    const T &operator [] (size_t i) const { return data[i]; }
    T &operator [] (size_t i) { return data[i]; }

    operator T * () { return data; }
};


template<typename T, size_t MSL=1000>
class lad_matrix {
    const size_t rows, cols;
    T sdata[MSL];
    T *data;

public:
    explicit lad_matrix(size_t rows_, size_t cols_) : rows(rows_), cols(cols_) {
        if (rows*cols <= MSL)
            data = &sdata[0];
        else
            data = new T[rows*cols];
    }

    ~lad_matrix() {
        if (rows*cols > MSL)
            delete [] data;
    }

    T const * operator[] (size_t i) const { return &data[cols*i]; }
    T * operator[] (size_t i) { return &data[cols*i]; }
};
4b9b3361

Ответ 1

Создайте большой буфер (MB +) в локальном хранилище потоков. (Фактическая память в куче, управление в TLS).

Разрешить клиентам запрашивать у него память в режиме FILO (подобный стеку). (это имитирует, как это работает в C VLA, и это эффективно, так как каждый запрос/возврат - это просто сложение/вычитание целого числа).

Получите свое хранилище VLA.

Обертка это довольно, так что вы можете сказать stack_array<T> x(1024); и есть, что stack_array иметь дело со строительством/уничтожения (заметим, что ->~T(), где T является int является юридическим Noop, и строительство может так же быть noop) или сделать stack_array<T> завернуть a std::vector<T, TLS_stack_allocator>.

Данные будут не такими локальными, как данные VLA C, потому что они будут эффективно работать в отдельном стеке. Вы можете использовать SBO (оптимизация небольших буферов), которая имеет место, когда действительно важна локальность.

A SBO stack_array<T> может быть реализован с помощью распределителя и std-вектора, объединенного с массивом std, или с уникальным ptr и обычным эсминцем, или множеством других способов. Возможно, вы можете модифицировать свое решение, заменив ваш новый /malloc/free/delete на вызовы в вышеупомянутое хранилище TLS.

Я говорю об использовании TLS, поскольку это устраняет необходимость в накладных расходах синхронизации, позволяя использовать многопоточность и отражает тот факт, что сам стек неявно является TLS.

Распределитель STL на основе стека? - это SO Q & A с по меньшей мере двумя "распределителями" стека в ответах. Им потребуется адаптация для автоматического получения своего буфера из TLS.

Обратите внимание, что TLS является одним большим буфером в некотором смысле деталью реализации. Вы можете делать большие выделения, и когда вы исчерпаете пространство, сделайте еще одно большое выделение. Вам просто нужно отслеживать текущую емкость каждой "стековой страницы" и список стековых страниц, поэтому, когда вы ее опустошаете, вы можете перейти на более ранний. Это позволяет вам быть немного более консервативным в своем первоначальном распределении TLS, не беспокоясь о запуске OOM; важная часть заключается в том, что вы FILO и выделяете редко, а не только, что весь буфер FILO является одним смежным.

Ответ 2

Я думаю, что вы уже перечисляли большинство вариантов в своем вопросе и комментариях.

  • Используйте std::vector. Это самый очевидный, самый беспроблемный, но, возможно, и самый медленный вариант.
  • Используйте расширения для конкретной платформы на тех платформах, которые их предоставляют. Например, GCC поддерживает массивы переменной длины на С++ в качестве расширения. POSIX указывает alloca, который широко поддерживается для выделения памяти в стеке. Даже Microsoft Windows предоставляет _malloca, как сказал мне быстрый веб-поиск.

    Чтобы избежать кошмаров обслуживания, вы действительно хотите инкапсулировать эти зависимости платформы в абстрактный интерфейс, который автоматически и прозрачно выбирает подходящий механизм для текущей платформы. Реализация этого для всех платформ будет немного полезной, но если эта единственная функция учитывает 3 & times; как вы сообщаете, это может стоить того. В качестве резерва для неизвестных платформ я оставил бы std::vector в резерве как последнее средство. Лучше работать медленно, но правильно, чем вести себя беспорядочно или вообще не запускаться.

  • Создайте свой собственный тип массива переменной величины, который реализует оптимизацию "малого массива", встроенную в буфер в самом объекте, как вы показали в своем вопросе. Я просто хочу отметить, что я скорее попытаюсь использовать union для std::array и std::vector вместо того, чтобы катить мой собственный контейнер.

    После того, как у вас есть настраиваемый тип, вы можете сделать интересное профилирование, например, поддерживать глобальную хеш-таблицу всех вхождений этого типа (по расположению исходного кода) и записывать каждый размер распределения во время стресс-теста вашей программы. Затем вы можете сбросить хеш-таблицу при выходе из программы и построить распределения в размерах размещения для отдельных массивов. Это может помочь вам точно настроить объем хранилища для резервирования для каждого массива отдельно в стеке.

  • Используйте std::vector с помощью специального распределителя. При запуске программы выделите несколько мегабайт памяти и передайте их в простой распределитель стека. Для распределителя стека выделение просто сравнивает и добавляет два целых числа, а освобождение - просто вычитание. Я сомневаюсь, что распределение стека, созданного компилятором, может быть намного быстрее. Ваш "массивный массив" затем будет пульсировать, соотнесенный с вашим "стеком программ". Эта конструкция также будет иметь преимущество, заключающееся в том, что случайный переполнения буфера - при этом все еще вызывает поведение undefined, удаляет случайные данные и все эти плохие вещи - не так легко повредит стек программы (обратные адреса), как это было бы с собственными VLA.

    Пользовательские распределители на С++ являются довольно грязным бизнесом, но некоторые люди сообщают, что они успешно используют их. (У меня нет большого опыта использования их самостоятельно.) Возможно, вы захотите начать поиск cppreference. Alisdair Meredith, который является одним из тех людей, которые рекламируют использование пользовательских распределителей, дал двухсессионный разговор на CppCon'14 под названием "Выполнение работ по распределению" (part 1, part 2), которые также могут оказаться интересными. Если интерфейс std::allocator слишком неудобен для вас, использование собственной переменной (в отличие от динамического) класса массива с вашим собственным распределителем также должно быть выполнимым.

Ответ 3

Что касается поддержки MSVC:

MSVC имеет _alloca, который выделяет пространство стека. Он также имеет _malloca, который выделяет пространство стека, если достаточно свободного пространства стека, в противном случае возвращается к динамическому распределению.

Вы не можете воспользоваться системой типа VLA, поэтому вам придется изменить свой код на работу, основанный на указателе на первый элемент такого массива.

Вам может понадобиться использовать макрос, который имеет разные определения в зависимости от платформы. Например. invoke _alloca или _malloca в MSVC, а также в g++ или других компиляторах, либо вызывает alloca (если они его поддерживают), либо делает VLA и указатель.


Рассматривайте способы перезаписи кода, не требуя выделения неизвестного количества стека. Одним из вариантов является выделение буфера фиксированного размера, который является максимальным, который вам понадобится. (Если это приведет к переполнению стека, это означает, что в любом случае ваш код прослушивается).