Почему использование alloca () не считается хорошей практикой? - PullRequest
364 голосов
/ 19 июня 2009

alloca() выделяет память в стеке, а не в куче, как в случае malloc(). Итак, когда я возвращаюсь из рутины, память освобождается. Итак, на самом деле это решает мою проблему освобождения динамически распределенной памяти. Освобождение памяти, выделенной через malloc(), является большой головной болью и, если ее пропустить, приводит к всевозможным проблемам с памятью.

Почему использование alloca() не рекомендуется, несмотря на вышеперечисленные функции?

Ответы [ 25 ]

216 голосов
/ 19 июня 2009

Ответ находится прямо на странице man (по крайней мере, на Linux ):

ВОЗВРАЩАЕМОЕ ЗНАЧЕНИЕ Функция alloca () возвращает указатель на начало выделенное пространство. Если причины распределения переполнение стека, поведение программы не определено.

Что не означает, что его никогда не следует использовать. Один из проектов OSS, над которым я работаю, широко использует его, и если вы не злоупотребляете им (alloca огромные значения), это нормально. Пройдя отметку «несколько сотен байт», пришло время использовать malloc и друзей. Вы все еще можете получить ошибки выделения, но, по крайней мере, у вас будет некоторое указание на ошибку, а не просто выброс стека.

188 голосов
/ 05 августа 2010

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

В заголовочном файле:

void DoSomething() {
   wchar_t* pStr = alloca(100);
   //......
}

В файле реализации:

void Process() {
   for (i = 0; i < 1000000; i++) {
     DoSomething();
   }
}

То, что произошло, было встроенной DoSomething функцией компилятора, и все выделения стека происходили внутри функции Process() и, таким образом, увеличивали стек. В мою защиту (и я не был тем, кто обнаружил проблему; мне пришлось пойти и поплакаться с одним из старших разработчиков, когда я не мог это исправить), это было не прямо alloca, это было одно из Макросы преобразования строк ATL.

Итак, урок - не используйте alloca в функциях, которые, по вашему мнению, могут быть встроенными.

69 голосов
/ 02 августа 2010

Старый вопрос, но никто не упомянул, что его следует заменить массивами переменной длины.

char arr[size];

вместо

char *arr=alloca(size);

Он находится в стандартном C99 и существовал как расширение компилятора во многих компиляторах.

57 голосов
/ 23 июня 2009

alloca () очень полезна, если вы не можете использовать стандартную локальную переменную, потому что ее размер должен быть определен во время выполнения, и вы можете абсолютно гарантирует, что указатель, полученный из alloca (), НИКОГДА не будет использоваться после того, как эта функция вернет .

Вы можете быть в полной безопасности, если вы

  • не возвращать указатель или что-либо, что содержит его.
  • не хранить указатель в какой-либо структуре, выделенной в куче
  • не разрешать другим потокам использовать указатель

Настоящая опасность возникает из-за того, что кто-то еще нарушит эти условия через некоторое время. Имея это в виду, он отлично подходит для передачи буферов функциям, которые форматируют текст в них:)

39 голосов
/ 19 июня 2009

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

  • Не все компиляторы поддерживают alloca.
  • Некоторые компиляторы по-разному интерпретируют предполагаемое поведение alloca, поэтому переносимость не гарантируется даже между компиляторами, которые его поддерживают.
  • Некоторые реализации содержат ошибки.
25 голосов
/ 19 июня 2009

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

21 голосов
/ 26 февраля 2013

все-таки использование alloca не рекомендуется, почему?

Я не вижу такого консенсуса. Много сильных плюсов; несколько минусов:

  • C99 предоставляет массивы переменной длины, которые часто используются преимущественно, поскольку обозначения более соответствуют массивам фиксированной длины и в целом интуитивно понятны
  • многие системы имеют меньше общей памяти / адресного пространства, доступного для стека, чем для кучи, что делает программу несколько более восприимчивой к исчерпанию памяти (через переполнение стека): это может рассматриваться как хорошее или плохое Дело в том, что одна из причин, по которой стек автоматически не увеличивается, как это происходит с кучи, состоит в том, чтобы не допустить столь неблагоприятного воздействия программ, вышедших из-под контроля, на всю машину
  • при использовании в более локальной области (например, в цикле while или for) или в нескольких областях объем памяти накапливается за одну итерацию / область и не освобождается до выхода из функции: это контрастирует с определенными обычными переменными в области структуры управления (например, for {int i = 0; i < 2; ++i) { X } будет накапливать alloca память, запрошенную в X, но память для массива фиксированного размера будет перерабатываться за итерацию).
  • современные компиляторы, как правило, не inline функции, которые вызывают alloca, но если вы их принудительно заставите, то alloca произойдет в контексте вызывающего (т. Е. Стек не будет освобожден, пока вызывающий не возвратится)
  • давно alloca перешли от непереносимой функции / хака к стандартизированному расширению, но некоторое негативное восприятие может сохраняться
  • время жизни связано с областью действия функции, которая может или не может подходить программисту лучше, чем явное управление malloc
  • необходимость использования malloc побуждает задуматься об освобождении - если это осуществляется с помощью функции-оболочки (например, WonderfulObject_DestructorFree(ptr)), то эта функция обеспечивает точку для операций очистки реализации (таких как закрытие дескрипторов файлов, освобождение внутренних указателей или ведение журнала) без явных изменений в клиентском коде: иногда это хорошая модель для последовательного принятия
    • в этом псевдо-ОО стиле программирования естественно хотеть что-то вроде WonderfulObject* p = WonderfulObject_AllocConstructor(); - это возможно, когда «конструктор» - это функция, возвращающая malloc -еданную память (так как память остается выделенной после того, как функция возвращает значение, которое будет сохранено в p), но не в том случае, если «конструктор» использует alloca
      • макро-версия WonderfulObject_AllocConstructor могла бы достичь этого, но «макросы - это зло» в том смысле, что они могут конфликтовать друг с другом и не-макро-кодом и создавать непреднамеренные замены и последующие трудно диагностируемые проблемы
    • пропущенные free операции могут быть обнаружены ValGrind, Purify и т. Д., Но пропущенные вызовы «деструктора» не всегда могут быть обнаружены вообще - одно очень незначительное преимущество с точки зрения обеспечения предполагаемого использования; некоторые реализации alloca() (например, GCC) используют встроенный макрос для alloca(), поэтому подстановка библиотеки диагностики использования памяти во время выполнения невозможна, как для malloc / realloc / free ( например, электрический забор)
  • у некоторых реализаций есть тонкие проблемы: например, из справочной страницы Linux:

    Во многих системах alloca () не может использоваться внутри списка аргументов вызова функции, потому что пространство стека, зарезервированное для alloca (), появится в стеке в середине пространства для аргументов функции.


Я знаю, что этот вопрос помечен C, но, как программист C ++, я подумал, что буду использовать C ++ для иллюстрации потенциальной полезности alloca: код ниже (и здесь, в ideone ) создает векторное отслеживание полиморфных типов разного размера, которые выделены в стеке (время жизни связано с возвратом функции), а не в куче.

#include <alloca.h>
#include <iostream>
#include <vector>

struct Base
{
    virtual ~Base() { }
    virtual int to_int() const = 0;
};

struct Integer : Base
{
    Integer(int n) : n_(n) { }
    int to_int() const { return n_; }
    int n_;
};

struct Double : Base
{
    Double(double n) : n_(n) { }
    int to_int() const { return -n_; }
    double n_;
};

inline Base* factory(double d) __attribute__((always_inline));

inline Base* factory(double d)
{
    if ((double)(int)d != d)
        return new (alloca(sizeof(Double))) Double(d);
    else
        return new (alloca(sizeof(Integer))) Integer(d);
}

int main()
{
    std::vector<Base*> numbers;
    numbers.push_back(factory(29.3));
    numbers.push_back(factory(29));
    numbers.push_back(factory(7.1));
    numbers.push_back(factory(2));
    numbers.push_back(factory(231.0));
    for (std::vector<Base*>::const_iterator i = numbers.begin();
         i != numbers.end(); ++i)
    {
        std::cout << *i << ' ' << (*i)->to_int() << '\n';
        (*i)->~Base();   // optionally / else Undefined Behaviour iff the
                         // program depends on side effects of destructor
    }
}
13 голосов
/ 19 июня 2009

Все остальные ответы верны. Однако, если вещь, которую вы хотите выделить с помощью alloca(), достаточно мала, я думаю, что это хорошая техника, которая быстрее и удобнее, чем malloc() или иным образом.

Другими словами, alloca( 0x00ffffff ) опасно и может вызвать переполнение, точно так же, как char hugeArray[ 0x00ffffff ];. Будьте осторожны и разумны, и вы будете в порядке.

11 голосов
/ 21 марта 2011

Все уже отметили большую вещь, которая заключается в потенциальном неопределенном поведении из-за переполнения стека, но я должен отметить, что в среде Windows есть отличный механизм, чтобы уловить это, используя структурированные исключения (SEH) и защитные страницы. Поскольку стек увеличивается только по мере необходимости, эти защитные страницы находятся в незанятых областях. Если вы выделяете их (переполняя стек), возникает исключение.

Вы можете поймать это исключение SEH и вызвать _resetstkoflw, чтобы сбросить стек и продолжить свой веселый путь. Это не идеально, но это еще один механизм, по крайней мере, знать, что что-то пошло не так, когда вещи попадают в фанат. * У nix может быть что-то похожее, чего я не знаю.

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

Кроме того, помимо предотвращения утечек памяти, alloca не вызывает фрагментации памяти, что очень важно. Я не думаю, что alloca - плохая практика, если вы используете ее разумно, что в принципе верно для всего. : -)

10 голосов
/ 01 апреля 2016

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

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

Я нашел эту цитату в .... Хорошо, я сделал эту цитату. Но на самом деле, подумай об этом ....

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

Я работал на настольных / серверных средах и встроенных системах. Многие встраиваемые системы вообще не используют кучу (они даже не ссылаются на ее поддержку) по причинам, которые включают в себя восприятие, что динамически выделяемая память является злом из-за риска утечек памяти в приложении, которое никогда не будет когда-либо перезагружается годами или более разумным обоснованием того, что динамическая память опасна, поскольку невозможно точно знать, что приложение никогда не фрагментирует свою кучу до точки ложного исчерпания памяти. Таким образом, у встроенных программистов остается несколько альтернатив.

alloca() (или VLA) может быть просто подходящим инструментом для работы.

Я снова и снова видел, как программист делает выделенный стеком буфер, «достаточно большой, чтобы обрабатывать любые возможные случаи». В глубоко вложенном дереве вызовов повторное использование этого (анти -?) Шаблона приводит к чрезмерному использованию стека. (Представьте себе дерево вызовов глубиной 20 уровней, где на каждом уровне по разным причинам функция слепо перераспределяет буфер в 1024 байта «просто для безопасности», когда обычно она использует только 16 или менее из них, и только в очень в редких случаях может использоваться больше.) Альтернатива - использовать alloca() или VLA и выделять только столько стекового пространства, сколько требуется вашей функции, чтобы избежать ненужной нагрузки на стек. Надеемся, что когда одна функция в дереве вызовов нуждается в распределении, превышающем нормальное, другие в дереве вызова все еще используют свои обычные небольшие выделения, и общее использование стека приложения значительно меньше, чем если бы каждая функция слепо перераспределяла локальный буфер .

Но если вы решите использовать alloca() ...

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

...