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

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

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

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

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

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


Контрольный показатель

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 векторы. Перехожу на heap-allocation выше размера 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]; }
};

person Szabolcs    schedule 03.04.2016    source источник
comment
Когда дело доходит до накладных расходов, ничто не заменит VLA. Хранилище для VLA совершенно бесплатно. Фактически, в большинстве случаев это совершенно бесплатно, сверх существующих накладных расходов на вызов функции. Ничего не получается лучше, чем стоимость 0%, поэтому, если MSVC не имеет VLA, у вас нет другого выбора, кроме как использовать другую альтернативу для VLA, что снизит производительность.   -  person Sam Varshavchik    schedule 03.04.2016
comment
@SamVarshavchik Мой вопрос: какова хорошая альтернатива, которая минимизирует снижение производительности? Есть один, который я показываю в вопросе. Не знаю, есть ли что-нибудь лучше. Я также не знаю, есть ли способ выделить память в стеке на C ++, который позволил бы мне имитировать работу VLA (возможно, один или несколько способов, зависящих от платформы ... Мне это нужно для Windows / Linux / OS X )   -  person Szabolcs    schedule 03.04.2016
comment
Если вы счастливы перейти к конкретной платформе, тогда GCC выполняет VLA как расширение и работает на всех этих платформах.   -  person Galik    schedule 03.04.2016
comment
@Galik Я могу в разумной степени перейти к конкретной платформе (то есть использовать вызовы, специфичные для ОС, в моей реализации класса массива). Но мне все еще нужен код для работы с MSVC. И я не хочу специализироваться ни на чем, кроме этого класса, для разных платформ. Можно ли использовать VLA внутри класса или обернуть его классом? До сих пор я понимал, что это не так. Но если это так, я бы сделал это (когда он скомпилирован с помощью gcc) и использовал вышеуказанное решение с MSVC.   -  person Szabolcs    schedule 03.04.2016
comment
Нет, это не так. Единственный вариант - разместить вектор / массив в куче. Вот и все.   -  person Sam Varshavchik    schedule 03.04.2016
comment
Также существует alloca (функция, специфичная для платформы, но существует в Linux / Windows / OS X): man7.org/linux/man-pages/man3/alloca.3.html Он динамически выделяет память в стеке.   -  person tmlen    schedule 03.04.2016
comment
@tmlen Можно ли использовать alloca для создания класса массива в стеке? Сложность, с которой я сталкиваюсь, заключается в том, что если я вызываю alloca в конструкторе класса, он будет использовать стек конструктора (и указатель не будет действительным после возврата конструктора).   -  person Szabolcs    schedule 04.04.2016
comment
Вы строите очень неправильное дерево, память VLA не быстрее, чем память new []. Вы должны задокументировать T, который вы использовали. Хрустальный шар говорит, что он плоский или двойной. Вы потеряли автоматическую векторизацию. Это делает код в 3-4 раза быстрее. Вам нужен как минимум VS2012, чтобы вернуть это. Используйте встроенные параметры диагностики автоматической векторизации и автоматического распараллеливания, чтобы узнать, что сделал оптимизатор.   -  person Hans Passant    schedule 04.04.2016
comment
@HansPassant T всегда int, char или bool в этом приложении, но никогда не является плавающей точкой. Это сложный комбинаторный алгоритм. Кроме того, если корень проблемы с производительностью заключается в том, что автоматическая векторизация теряется, то почему оптимизация, которую я пробовал, работает так же хорошо?   -  person Szabolcs    schedule 04.04.2016
comment
@Hans Я думаю, что мой исходный пост вводил в заблуждение. Я показываю тесты с clang, а не с MSVC. Код по-прежнему должен работать с MSVC, но я работаю в основном над OS X и при необходимости тестирую в Windows.   -  person Szabolcs    schedule 04.04.2016
comment
Можете показать код теста? Возможно, есть способ использовать больше памяти для повышения скорости?   -  person rubenvb    schedule 04.04.2016
comment
alloca нужно будет вызвать в функции, стек которой следует использовать. То есть не в конструкторе векторного класса (или в списке инициализации). Класс может принимать указатель в качестве аргумента конструктора, например lad_vector vec( (int*)alloca(10 * sizeof(int)), 10 );. Возможно, сделайте для этого макрос (но не встроенную функцию), чтобы получить синтаксис типа lad_vector vec = MAKE_LADVECTOR(10);   -  person tmlen    schedule 04.04.2016
comment
Я думаю, что ваш C99 сломан, вы не можете переносить такие большие VLA. Я удивлен, что у вас никогда не бывает переполнения стека. Кажется странным, что вы получаете лучшую производительность при очень высоких значениях MSL: для такого большого количества данных я ожидал бы, что стоимость выделения кучи будет очень маленькой по сравнению со стоимостью обработки данных.   -  person Benoît    schedule 04.04.2016
comment
@ Benoît Я думаю, что реализация много выделяет внутри тяжелого вычислительного кода, по крайней мере, так кажется. Однако это не объясняет улучшения, наоборот.   -  person rubenvb    schedule 04.04.2016
comment
Увеличение MSL еще больше улучшает производительность, но в конечном итоге программа начнет возвращать неправильные результаты (я полагаю, из-за переполнения стека). Я не понимаю, как переполнение стека может дать вам неправильные результаты. В любой нормальной системе в худшем случае вы должны получить segfault. (За исключением чего-то очень необычного, например, такого большого переполнения, что вы попадаете в какую-то другую область действующей памяти.) Так что, возможно, вам стоит поискать ошибку.   -  person Nate Eldredge    schedule 04.04.2016
comment
Может ли это быть актуальным или полезным: Ищете векторный класс в стиле C ++ STL, но с использованием хранилища стека   -  person Nathan Cooper    schedule 04.04.2016


Ответы (3)


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

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

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

Оберните его красиво, чтобы вы могли сказать stack_array<T> x(1024); и stack_array иметь дело со строительством / разрушением (обратите внимание, что ->~T(), где T равно 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.

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

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

person Yakk - Adam Nevraumont    schedule 03.04.2016
comment
Интересная идея, попробую. Что такое SBO? - person Szabolcs; 04.04.2016
comment
Я хотел бы знать, почему это было отклонено. Вариант использования заменяет C99 VLA в коде, изначально написанном на C99. Это означает, что массивы всегда уничтожаются в порядке, обратном их созданию, поэтому идея взять их хранилище из управляемого вручную стека должна сработать ... Если есть ожидаемая проблема, я хотел бы знать. - person Szabolcs; 04.04.2016
comment
@sza небольшая оптимизация буфера (что вы уже пробовали), локальное хранение небольших массивов. На самом деле, попробуйте только в том случае, если вышеупомянутое сначала не проходит тесты производительности. - person Yakk - Adam Nevraumont; 04.04.2016
comment
@Szabolcs Как теория, за исключением деталей TLS, мой ответ совпадает с последней идеей 5gon12eder; может кому-то не понравилось, насколько они похожи. Если бы деталь TLS была сложена в ответ 5gon12, мой был бы лишним; в то же время я сильно подозреваю, что это единственное решение, которое может решить как проблемы с переносимостью, так и с производительностью. - person Yakk - Adam Nevraumont; 04.04.2016
comment
Ваша идея с FILO работает хорошо и сокращает разрыв в производительности. Я приму ответ, как только завершу реализацию (возможно, завтра). - person Szabolcs; 04.04.2016

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

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

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

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

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

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

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

person 5gon12eder    schedule 03.04.2016
comment
Объединение классов звучит опасно, деструкторы для объединений не выполняются. - person Alexander Oh; 03.04.2016
comment
@Alex Это безопасно, так как C ++ 11. Конечно, вы должны позаботиться о том, чтобы ваши деструкторы вызывали соответствующий деструктор текущего активного члена union. - person 5gon12eder; 04.04.2016
comment
Распределители пула с std::vector должны быть лучшими из всех миров. - person Lightness Races in Orbit; 04.04.2016

По поводу поддержки MSVC:

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

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

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


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

person M.M    schedule 03.04.2016
comment
Я бы побеспокоился о том, что alloca использует неправильный фрейм стека, если он не вызывается явно из той же функции, в которой объявлен объект. - person Random832; 04.04.2016
comment
@ Random832 не уверен, о чем вы говорите, я предлагаю заменить объявления VLA на alloca в качестве возможного варианта - person M.M; 04.04.2016
comment
Думаю, я запутался и подумал, что вы говорите о сокрытии этого поведения за классом. - person Random832; 04.04.2016
comment
@ Random832: И _alloca(), и alloca() поступают правильно, если вызов функции, в которой они используются, правильно встроен. Вы можете убедиться в этом, используя __forceinline и __attribute__((always_inline)). Я широко использую это в коде C90 (который также не имеет VLA). - person alecov; 04.04.2016