Замена C++ для C99 VLAs (цель: сохранить производительность)

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

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

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

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

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


Benchmark

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

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

повышение 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]; }
};

3 ответов


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

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

получите ваше хранилище VLA от него.

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

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

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

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

Stack-buffer на основе распределителя STL? это так Q&A С по крайней мере двумя распределителями "стека" в ответах. Им понадобится некоторая адаптация, чтобы автоматически получить их буфер из TLS.

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


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

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

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

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

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

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

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


относительно поддержки MSVC:

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

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

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


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