Массив или Маллок?


13

Я использую следующий код в моем приложении, и он работает нормально. Но мне интересно, лучше ли сделать это с помощью malloc или оставить все как есть?

function (int len)
{
char result [len] = some chars;
send result over network
}

2
Является ли предположение, что код предназначен для не встроенной среды?
Технит

Ответы:


28

Основное отличие состоит в том, что VLA (массивы переменной длины) не предоставляют механизма для обнаружения ошибок распределения.

Если вы объявите

char result[len];

и lenпревышает количество доступного стекового пространства, поведение вашей программы не определено. Не существует языкового механизма для определения заранее, будет ли выделение выполнено успешно, или для определения того, успешно ли оно выполнено.

С другой стороны, если вы напишите:

char *result = malloc(len);
if (result == NULL) {
    /* allocation failed, abort or take corrective action */
}

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

(Ну, в основном. В системах Linux malloc()можно выделить часть адресного пространства, даже если нет доступного соответствующего хранилища; более поздние попытки использовать это пространство могут вызвать OOM Killer . Но проверка на malloc()сбой все еще является хорошей практикой.)

Еще одна проблема во многих системах заключается в том, что доступно больше места (возможно, намного больше места), malloc()чем для автоматических объектов, таких как VLA.

И, как уже упоминался ответ Филиппа, VLA были добавлены в C99 (в частности, Microsoft их не поддерживает).

И VLA были сделаны необязательными в C11. Вероятно, большинство компиляторов C11 будут их поддерживать, но вы не можете на это рассчитывать.


14

Автоматические массивы переменной длины были введены в C в C99.

Если у вас нет опасений по поводу обратной сопоставимости со старыми стандартами, это нормально.

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


7
Я должен не согласиться с изречением «если это работает, не трогай это». Ошибочно полагать, что какой-то код «работает», может заставить вас обойти проблемы в некотором коде, который «работает». Вера должна быть заменена предварительным принятием того, что какой-то код работает прямо сейчас.
Брюс Эдигер

2
Не трогайте его, пока не сделаете версию, может быть, «лучше» работает ...
H_7

8

Если ваш компилятор поддерживает массивы переменной длины, единственной опасностью является переполнение стека в некоторых системах, когда они lenневероятно велики. Если вы точно знаете, что lenоно не будет больше определенного числа, и знаете, что ваш стек не будет переполнен даже при максимальной длине, оставьте код как есть; в противном случае перепишите его с помощью mallocи free.


как насчет функции non c99 (char []) {char result [sizeof (char)] = некоторые символы; отправить результат по сети}
Dev Bag

@DevBag char result [sizeof(char)]- это массив размера 1(потому что sizeof(char)равен единице), поэтому назначение будет усечено some chars.
dasblinkenlight

извините за это, я имею в виду этот способ function (char str []) {char result [sizeof (str)] = некоторые символы; отправить результат по сети}
Dev Bag

4
@DevBag Это тоже не сработает - str затухает до указателя , поэтому его sizeofбудет четыре или восемь, в зависимости от размера указателя в вашей системе.
dasblinkenlight

2
Если вы используете версию C без массивов переменной длины, вы можете сделать это char* result = alloca(len);, что выделяет в стеке. Он имеет тот же основной эффект (и те же основные проблемы)
Gort the Robot

6

Мне нравится идея, что у вас может быть выделенный во время выполнения массив без фрагментации памяти, висячих указателей и т. Д. Однако другие отмечают, что это распределение во время выполнения может молча завершиться сбоем. Поэтому я попробовал это с помощью gcc 4.5.3 в среде Cygwin Bash:

#include <stdio.h>
#include <string.h>

void testit (unsigned long len)
{
    char result [len*2];
    char marker[100];

    memset(marker, 0, sizeof(marker));
    printf("result's size: %lu\n", sizeof(result));
    strcpy(result, "this is a test that should overflow if no allocation");
    printf("marker's contents: '%s'\n", marker);
}

int main(int argc, char *argv[])
{
    testit(100);
    testit((unsigned long)-1);  // probably too big
}

Выход был:

$ ./a.exe
result's size: 200
marker's contents: ''
result's size: 4294967294
marker's contents: 'should overflow if no allocation'

Чрезмерно большая длина, передаваемая во втором вызове, явно вызывала сбой (перетекание в маркер []). Это не означает, что этот вид проверки защищен от дурака (глупцы могут быть умными!) Или что он соответствует стандартам C99, но это может помочь, если у вас есть такая проблема.

Как обычно, YMMV.


1
+1 это очень полезно: 3
Kokizzu

Всегда приятно иметь какой-то код, чтобы соответствовать заявлениям людей! Спасибо ^ _ ^
Муса Аль-Хасси

3

Вообще говоря, стек - это самое простое и лучшее место для размещения ваших данных.

Я бы избежал проблем с VLA, просто выделив самый большой массив, который вы ожидаете.

Однако бывают случаи, когда куча лучше и возиться с malloc стоит усилий.

  1. Когда его большой, но переменный объем данных. Большое зависит от вашей среды> 1 КБ для встроенных систем,> 10 МБ для корпоративного сервера.
  2. Когда вы хотите, чтобы данные сохранялись после выхода из процедуры, например, если вы возвращаете указатель на свои данные. С помощью
  3. Комбинация статического указателя и malloc () обычно лучше, чем определение большого статического массива;

3

Во встроенном программировании мы всегда используем статический массив вместо malloc, когда часто используются операции malloc и free. Из-за отсутствия управления памятью во встроенной системе частые операции выделения и освобождения приводят к фрагменту памяти. Но мы должны использовать некоторые хитрые методы, такие как определение максимального размера массива и использование статического локального массива.

Если ваше приложение работает в Linux или Windows, это не имеет значения, используя массив или malloc. Ключевой момент заключается в том, где вы используете свою структуру дат и логику кода.


1

Что еще никто не упомянул, так это то, что опция массива переменной длины, вероятно, будет намного быстрее, чем malloc / free, поскольку выделение VLA - это всего лишь случай настройки указателя стека (по крайней мере, в GCC).

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


1
Это будет казаться хорошим до тех пор, пока не столкнет вас с ситуацией вне стека. Более того, это может быть не ваш код, который фактически достигает предела стека; это может закончиться кусанием в библиотеке или системном вызове (или прерывании).
Donal Fellows

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

1

Это очень распространенное C-решение, которое я использую для решения проблемы, которая может помочь. В отличие от VLA, он не сталкивается с практическим риском переполнения стека в патологических случаях.

/// Used for frequent allocations where the common case generally allocates
/// a small amount of memory, at which point a heap allocation can be
/// avoided, but rare cases also need to be handled which may allocate a
/// substantial amount. Note that this structure is not safe to copy as
/// it could potentially invalidate the 'data' pointer. Its primary use
/// is just to allow the stack to be used in common cases.
struct FastMem
{
    /// Stores raw bytes for fast access.
    char fast_mem[512];

    /// Points to 'fast_mem' if the data fits. Otherwise, it will point to a
    /// dynamically allocated memory address.
    void* data;
};

/// @return A pointer to a newly allocated memory block of the specified size.
/// If the memory fits in the specified fast memory structure, it will use that
/// instead of the heap.
void* fm_malloc(struct FastMem* mem, int size)
{
    // Utilize the stack if the memory fits, otherwise malloc.
    mem->data = (size < sizeof mem->fast_mem) ? mem->fast_mem: malloc(size);
    return mem->data;
}

/// Frees the specified memory block if it has been allocated on the heap.
void fm_free(struct FastMem* mem)
{
    // Free the memory if it was allocated dynamically with 'malloc'.
    if (mem->data != mem->fast_mem)
        free(mem->data);
    mem->data = 0;
}

Чтобы использовать его в вашем случае:

struct FastMem fm;

// `result` will be allocated on the stack if 'len <= 512'.
char* result = fm_malloc(&fm, len);

// send result over network.
...

// this function will only do a heap deallocation if 'len > 512'.
fm_free(&fm, result);

В этом случае это использует стек, если строка помещается в 512 байтов или меньше. В противном случае он использует распределение кучи. Это может быть полезно, если, скажем, в 99% случаев строка помещается в 512 байтов или меньше. Однако, скажем, есть какой-то сумасшедший экзотический случай, который вам иногда может понадобиться, когда строка занимает 32 килобайта, когда пользователь засыпает на клавиатуре или что-то в этом роде. Это позволяет обрабатывать обе ситуации без проблем.

Фактическая версия Я использую в производстве также имеет свою собственную версию reallocи callocтак далее, а также соответствующую стандарту структуры C ++ данных , построенной на ту же концепцию, но я извлек необходимый минимум , чтобы проиллюстрировать эту концепцию.

У него есть предостережение, что копировать его опасно, и вы не должны возвращать указатели, выделенные через него (они могут в конечном итоге стать недействительными, поскольку FastMemэкземпляр уничтожен). Он предназначен для использования в простых случаях в области действия локальной функции, где у вас возникнет соблазн всегда использовать стек / VLA, в противном случае в некоторых редких случаях могут возникнуть переполнения буфера / стека. Это не универсальный распределитель и не должен использоваться как таковой.

Я на самом деле создал его давным-давно в ответ на ситуацию в унаследованной кодовой базе с использованием C89, когда бывшая команда думала, что никогда не случится, когда пользователю удастся назвать элемент с именем длиной более 2047 символов (возможно, он заснул на своей клавиатуре ). Мои коллеги на самом деле пытались увеличить размер массивов, распределенных в разных местах, до 16384, и в этот момент я подумал, что это становится нелепым и просто меняет больший риск переполнения стека в обмен на меньший риск переполнения буфера. Это дало решение, которое было очень легко подключить, чтобы исправить эти случаи, просто добавив пару строк кода. Это позволило обрабатывать общий случай очень эффективно и по-прежнему использовать стек без тех сумасшедших редких случаев, которые требовали сбой программного обеспечения. Тем не менее, я' с тех пор мы нашли его полезным даже после C99, поскольку VLA по-прежнему не могут защитить нас от переполнения стека. Это можно, но все еще пулы из стека для небольших запросов на выделение.


1

Стек вызовов всегда ограничен. В основных операционных системах, таких как Linux или Windows, ограничение составляет один или несколько мегабайт (и вы можете найти способы изменить его). В некоторых многопоточных приложениях оно может быть ниже (поскольку потоки могут быть созданы с меньшим стеком). На встроенных системах он может составлять всего несколько килобайт. Хорошее эмпирическое правило - избегать фреймов вызовов размером более нескольких килобайт.

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

Тем не менее, использование ручного динамического выделения памяти C (например, callocили malloc&free ) также имеет свои недостатки:

  • он может потерпеть неудачу, и вы всегда должны проверять на неудачу (например, callocили mallocвозвращаться NULL).

  • это медленнее: успешное выделение VLA занимает несколько наносекунд, успешному mallocможет потребоваться несколько микросекунд (в хороших случаях - всего лишь доли микросекунды) или даже больше (в патологических случаях, связанных с избиением , гораздо больше).

  • это гораздо сложнее кодировать: вы можете freeтолько тогда, когда вы уверены, что указанная зона больше не используется. В вашем случае вы можете звонить callocи freeв том же порядке.

Если вы знаете, что в большинстве случаев ваше result (очень плохое имя, вы никогда не должны возвращать адрес автоматической переменной VLA; так что я использую bufвместо resultприведенного ниже) мало, вы можете использовать его в особых случаях, например

char tinybuf[256];
char *buf = (len<sizeof(tinybuf))?tinybuf:malloc(len);
if (!buf) { perror("malloc"); exit(EXIT_FAILURE); };
fill_buffer(buf, len);
send_buffer_on_network(buf, len);
if (buf != tinybuf) 
  free(buf);

Однако приведенный выше код менее читабелен и, вероятно, является преждевременной оптимизацией. Это, однако, более надежно, чем чистое решение VLA.

PS. Некоторые системы (например, некоторые дистрибутивы Linux по умолчанию включены) имеют чрезмерную загрузку памяти (что дает mallocуказатель, даже если памяти недостаточно). Эта функция мне не нравится и обычно отключается на моих компьютерах с Linux.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.