Я использую следующий код в моем приложении, и он работает нормально. Но мне интересно, лучше ли сделать это с помощью malloc или оставить все как есть?
function (int len)
{
char result [len] = some chars;
send result over network
}
Я использую следующий код в моем приложении, и он работает нормально. Но мне интересно, лучше ли сделать это с помощью malloc или оставить все как есть?
function (int len)
{
char result [len] = some chars;
send result over network
}
Ответы:
Основное отличие состоит в том, что 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 будут их поддерживать, но вы не можете на это рассчитывать.
Автоматические массивы переменной длины были введены в C в C99.
Если у вас нет опасений по поводу обратной сопоставимости со старыми стандартами, это нормально.
В общем, если это работает, не трогайте это. Не оптимизируйте заранее. Не беспокойтесь о добавлении специальных функций или умных способов ведения дел, потому что вы часто не собираетесь его использовать. Будь проще.
Если ваш компилятор поддерживает массивы переменной длины, единственной опасностью является переполнение стека в некоторых системах, когда они len
невероятно велики. Если вы точно знаете, что len
оно не будет больше определенного числа, и знаете, что ваш стек не будет переполнен даже при максимальной длине, оставьте код как есть; в противном случае перепишите его с помощью malloc
и free
.
char result [sizeof(char)]
- это массив размера 1
(потому что sizeof(char)
равен единице), поэтому назначение будет усечено some chars
.
str
затухает до указателя , поэтому его sizeof
будет четыре или восемь, в зависимости от размера указателя в вашей системе.
char* result = alloca(len);
, что выделяет в стеке. Он имеет тот же основной эффект (и те же основные проблемы)
Мне нравится идея, что у вас может быть выделенный во время выполнения массив без фрагментации памяти, висячих указателей и т. Д. Однако другие отмечают, что это распределение во время выполнения может молча завершиться сбоем. Поэтому я попробовал это с помощью 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.
Вообще говоря, стек - это самое простое и лучшее место для размещения ваших данных.
Я бы избежал проблем с VLA, просто выделив самый большой массив, который вы ожидаете.
Однако бывают случаи, когда куча лучше и возиться с malloc стоит усилий.
Во встроенном программировании мы всегда используем статический массив вместо malloc, когда часто используются операции malloc и free. Из-за отсутствия управления памятью во встроенной системе частые операции выделения и освобождения приводят к фрагменту памяти. Но мы должны использовать некоторые хитрые методы, такие как определение максимального размера массива и использование статического локального массива.
Если ваше приложение работает в Linux или Windows, это не имеет значения, используя массив или malloc. Ключевой момент заключается в том, где вы используете свою структуру дат и логику кода.
Что еще никто не упомянул, так это то, что опция массива переменной длины, вероятно, будет намного быстрее, чем malloc / free, поскольку выделение VLA - это всего лишь случай настройки указателя стека (по крайней мере, в GCC).
Таким образом, если эта функция часто вызывается (которую вы, конечно, определите с помощью профилирования), VLA является хорошим вариантом оптимизации.
Это очень распространенное 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 по-прежнему не могут защитить нас от переполнения стека. Это можно, но все еще пулы из стека для небольших запросов на выделение.
Стек вызовов всегда ограничен. В основных операционных системах, таких как 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.