Уловка, чтобы избежать необходимости инициализировать массив

Обычно, если я хочу выделить массив с нулевой инициализацией, я бы сделал что-то вроде этого:

int size = 1000;
int* i = (int*)calloc(sizeof int, size));

И позже мой код может сделать это, чтобы проверить, был ли инициализирован элемент в массиве:

if(!i[10]) {
  // i[10] has not been initialized
}

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

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

Я думаю, это было что-то вроде этого:

int size = 1000;
int* i = (int*)malloc(size*sizeof int));
int* i_markers = (int*)malloc(size*sizeof int));

Если используется запись в массиве, она записывается следующим образом:

i_markers[10] = &i[10];

И затем его использование можно проверить позже следующим образом:

if(i_markers[10] != &i[10]) {
  // i[10] has not been initialized
}

Конечно, это не совсем правильно, потому что i_markers[10] могло быть случайным образом установлено на &i[10].

Может кто напомнит технику?

Благодарю вас!


Кажется, я это запомнил. Это правильно? Есть ли лучший способ или есть варианты? Спасибо еще раз. (Это было обновлено, чтобы быть правильным ответом)

struct lazy_array {
    int size;
    int* values;
    int* used;
    int* back_references;
    int num_used;
};

struct lazy_array* create_lazy_array(int size) {
    struct lazy_array* lazy = (struct lazy_array*)malloc(sizeof(lazy_array));
    lazy->size = 1000;
    lazy->values = (int*)malloc(size*sizeof int));
    lazy->used = (int*)malloc(size*sizeof int));
    lazy->back_references = (int*)malloc(size*sizeof int));
    lazy->num_used = 0;
    return lazy;
}

void use_index(struct lazy_array* lazy, int index, int value) {
    lazy->values[index] = value;
    if(is_index_used(lazy, index))
      return;
    lazy->used[index] = lazy->used;
    lazy->back_references[lazy->used[index]] = index;
    ++lazy->used;
}

int is_index_used(struct lazy_array* lazy, int index) {
    return lazy->used[index] < lazy->num_used &&
        lazy->back_references[lazy->used[index]] == index);
}

person John Cashew    schedule 02.10.2015    source источник
comment
Не приводить результат malloc.   -  person Ilya    schedule 02.10.2015
comment
как вы можете сказать, что пытаетесь минимизировать время выполнения (или любой другой ресурс), а затем предлагаете вдвое больше ресурса кучи и дополнительный код для установки/проверки содержимого второго массива.   -  person user3629249    schedule 02.10.2015
comment
Интересно прочитать еще один вопрос о calloc. Я предлагаю прочитать всю ветку, особенно комментарии под ответами, получившими наибольшее количество голосов. У меня сложилось впечатление, что использование calloc в современной операционной системе должно превзойти ленивый массив.   -  person user3386109    schedule 02.10.2015
comment
если массив объявлен в глобальном адресном пространстве файла, то он будет установлен на все 0x00 по мере загрузки процесса и прохождения инициализации процесса   -  person user3629249    schedule 02.10.2015
comment
@Ilya Код приводит результат malloc для совместимости с C ++.   -  person John Cashew    schedule 02.10.2015
comment
@user3629249 user3629249 в этом случае я хочу использовать больше памяти, чтобы меньше использовать процессор. Это то, что желательно в данной ситуации.   -  person John Cashew    schedule 02.10.2015
comment
@user3386109 user3386109 Упомянутая статья — это не вся история, и она вводит в заблуждение. calloc часто возвращает память из кучи, а не из анонимного сегмента (как и в случае с malloc, это в основном зависит от размера выделения, параметров malloc, библиотеки malloc, настраиваемых параметров malloc и политики чрезмерной фиксации ОС). Даже без этих факторов ленивый массив по-прежнему подходит для другого варианта использования, чем calloc.   -  person John Cashew    schedule 02.10.2015
comment
В некоторых операционных системах (например, Linux) большой calloc равен или быстрее, чем большой malloc, потому что ОС в любом случае предоставляет виртуальные страницы, уже обнуленные. Так что отложите преждевременную оптимизацию, если только вы не продемонстрировали необходимость в тестировании!   -  person M.M    schedule 02.10.2015
comment
@MM Возможности и цели производительности системы определяют, какие структуры данных требуются. Тестирование мало чем поможет вам сделать выбор между алгоритмом O(1) и O(N).   -  person John Cashew    schedule 02.10.2015
comment
@JohnCashew, код не C++, иначе malloc/calloc не использовался бы. Таким образом, нет необходимости включать какую-либо совместимость с C++. Эта совместимость с C++ просто загромождает код и увеличивает головную боль при обслуживании. Кроме того, глобальное пространство файла всегда устанавливается равным 0x00 во время загрузки процесса, поэтому он не требует дополнительных циклов ЦП.   -  person user3629249    schedule 02.10.2015
comment
@user3629249 user3629249 Это код C++. malloc/calloc иногда подходят для использования в C++. Также это структура данных переменного размера общего назначения. Использование глобального пространства не было бы подходящим решением.   -  person John Cashew    schedule 02.10.2015
comment
i_markers[10] = &i[10]; - это проблема, назначение указателя на int. Почему бы не использовать int** i_markers?   -  person chux - Reinstate Monica    schedule 02.10.2015


Ответы (3)


В большинстве компиляторов/стандартных библиотек, о которых я знаю, большие calloc запросы (и malloc в этом отношении) реализуются с точки зрения логики запросов объемной памяти ОС. В Linux это означает копирование при записи mmap нулевой страницы, а в Windows это означает VirtualAlloc. В обоих случаях ОС дает вам уже нулевую память, и calloc распознает это; он явно обнуляет память только в том случае, если он делал небольшую calloc из небольшой кучи распределения. Так что, пока вы не напишете на любую заданную страницу в распределении, это ноль «бесплатно». Не нужно явно лениться; распределитель ленив для вас.

Для небольших выделений действительно необходимо memset очистить память, но тогда довольно дешево memset memset несколько тысяч байт (или десятков тысяч) байтов. Для действительно больших распределений, где обнуление было бы дорогостоящим, вы получаете предоставляемую ОС память, которая обнуляется бесплатно (отдельно от остальной части кучи); например для dlmalloc в типичной конфигурации выделения за пределами 256 КБ всегда будут заново mmap-ed и munmap-ed, что означает, что вы получаете свежесопоставленные сопоставления копирования при записи нулевой страницы (стоимость их обнуления откладывается до тех пор, пока вы не выполнить запись где-нибудь на странице и оплатить, получили ли вы 256 КБ через malloc или calloc).

Если вам нужны лучшие гарантии обнуления или бесплатное обнуление при меньших выделениях (хотя это более расточительно, чем ближе к одной странице вы получаете), вы можете просто явно сделать то, что malloc/calloc делают неявно, и использовать предоставленную ОС обнуленную память. , например заменять:

sometype *x = calloc(num, sizeof(*x)); // Or the similar malloc(num * sizeof(*x));
if (!x) { ... do error handling stuff ... }
...
free(x);

либо с:

sometype *x = mmap(NULL, num * sizeof(*x), PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
if (x == MAP_FAILED) {  ... do error handling stuff ... }
...
munmap(x, num * sizeof(*x));

или в Windows:

sometype *x = VirtualAlloc(NULL, num * sizeof(*x),  MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!x) { ... do error handling stuff ... }
...
VirtualFree(x, 0, MEM_RELEASE); // VirtualFree with MEM_RELEASE only takes size of 0

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

person ShadowRanger    schedule 02.10.2015
comment
Благодарю за ваш ответ. Однако это не так. В обоих случаях ОС дает вам уже нулевую память. если память не равна нулю, то ОС должна установить ее в ноль. - person John Cashew; 02.10.2015
comment
В случае с Linux используется система виртуальной памяти для копирования при записи mmap нулевой страницы. Он не назначает ему никакой оперативной памяти, он просто настраивает таблицы страниц для ссылки на общие нули. Эти таблицы ссылаются на то, что уже равно нулю; только когда вы пишете в него, новая реальная страница инициализируется нулями (плюс все, что вы в нее написали). Механизм в VirtualAlloc может отличаться, но в любом случае его нельзя избежать; ОС не будет возвращать неинициализированную память, потому что это представляет угрозу безопасности; что, если какая-то другая программа ранее хранила там конфиденциальные данные? - person ShadowRanger; 02.10.2015
comment
В любом случае, у вас нет выбора в этом вопросе. Большие malloc делают то же самое, что и большие calloc. Если вы просите много памяти, она уже обнулена; бесплатно это или нет, не имеет значения, потому что, даже если обнуление чего-то стоит, у вас нет другого выбора, кроме как заплатить; ОС берет оплату (если таковая имеется; как я уже сказал, в Linux ее определенно нет, а Windows может в фоновом режиме очищать VirtualFree-ed память независимо от выполнения вашей программы) без вашего участия, прежде чем у вас появится шанс на объект. - person ShadowRanger; 02.10.2015
comment
В случае, когда библиотека malloc может выделить память из памяти, уже обработанной sbrk() или mmap(), malloc не возвращает нулевую инициализированную память, а calloc возвращает. - person John Cashew; 02.10.2015
comment
О, и подтверждение на VirtualAlloc: если вы посмотрите на код ядра, основные блоки физической памяти обнуляются, прежде чем становятся доступными для использования диспетчером памяти ядра. Источник - person ShadowRanger; 02.10.2015
comment
нет необходимости подтверждать это. любая современная ОС делает это в качестве меры безопасности. - person John Cashew; 02.10.2015
comment
Я согласен, что malloc и calloc не дают гарантий (malloc в отношении того, всегда ли некоторые выделения на самом деле равны нулю, calloc в отношении того, является ли обнуление явным или неявным), но, по крайней мере, в Linux, Windows и OpenBSD со стандартными библиотеками времени выполнения C это как это работает. Порог для бесплатных нулей отличается (для dlmalloc порог обычно составляет 256 КБ, хотя страницы обычно имеют размер 4 КБ), но это означает, что существует предел оплачиваемой стоимости. Некоторые подробности указаны в Википедии. - person ShadowRanger; 02.10.2015
comment
Дело в том, что в течение жизни долгоживущего процесса calloc(), скорее всего, будет операцией O(N), а malloc — операцией O(1). @ShadowRanger - person John Cashew; 02.10.2015
comment
Нет, это моя точка зрения. Запросы bigbin (как их называет dlmalloc) не зависят от остальной части распределителя. Когда сделан большой запрос (malloc или calloc), он обновляется mmap. Когда это free-ed, оно немедленно munmap-ed; даже если вы запросите блок того же размера через микросекунду, он будет mmap свежим (что означает ленивый CoW, снова отображающий нулевую страницу). Неважно, используете ли вы calloc или malloc, когда запрос достаточно велик. Для небольших запросов да, calloc потребуется memset, malloc нет. Но обнуление небольших запросов редко имеет значение для производительности. - person ShadowRanger; 02.10.2015
comment
выбор типа mmap() или sbrk()ed зависит от многих других факторов, например, от количества и фрагментации памяти, выделяемой sbrk(), настроек mallopt и библиотеки malloc. Несмотря на это, я не хочу платить за обнуление памяти, которую не нужно обнулять даже при копировании при записи. Если я не хочу платить библиотеке malloc 1000 долларов, зачем мне платить ей 100 долларов? - person John Cashew; 02.10.2015
comment
лениво обнуляется в фоновом режиме между запросами. Это пустая трата процессора. Пока ОС обнуляет эту память, мой процесс будет приостановлен. - person John Cashew; 02.10.2015
comment
Конечно, если вы планируете удерживать память, а не освобождать ее, и планируете повторно использовать ее как логически новую память, вы можете использовать ленивые приемы, чтобы никто никогда не обнулил ее. Я не уверен, что это даже обеспечит значительное улучшение производительности (преждевременная оптимизация — корень всех зол, бла-бла, но это забавное зло!), но это один из сценариев, где это может помочь. - person ShadowRanger; 02.10.2015
comment
Вы, наконец, признали и/или поняли назначение этой структуры данных! - person John Cashew; 02.10.2015
comment
Ну, вы изначально описали это как новые выделения, а не повторное использование вашей собственной памяти, отсюда и недоразумение. Кстати, я был прав насчет того, что Windows лениво обнуляет VirtualFree страницы в фоновом режиме, но, по-видимому, она также лениво поддерживает их; выделения не поддерживаются до тех пор, пока не будут получены; после доступа им даются страницы из лениво обнуленного набора бесплатных страниц. Подробности здесь: randomascii.wordpress.com/2014/ 10/12/ Так что да, если у него закончатся обнуленные страницы, вы можете заблокировать обнуление при доступе к странице, заплатив ненужные вам расходы. - person ShadowRanger; 02.10.2015

Это можно сделать, хотя это зависит от неопределенного поведения. Он называется ленивым массивом.

Хитрость заключается в использовании таблицы обратного поиска. Каждый раз, когда вы сохраняете значение, вы сохраняете его индекс в ленивом массиве:

void store(int value)
{
   if (is_stored(value)) return;
   lazy_array[value] = next_index;
   table[next_index] = value;
   ++next_index;
}

int is_stored(int value)
{
  if (lazy_array[value]<0) return 0;
  if (lazy_array[value]>=next_index) return 0;
  if (table[lazy_array[value]]!=value) return 0;
  return 1;
}

Идея состоит в том, что если значение не было сохранено в ленивом массиве, то lazy_array[value] будет мусором. Его значение будет либо недопустимым индексом, либо допустимым индексом в вашей таблице обратного поиска. Если это недопустимый индекс, то вы сразу узнаете, что там ничего не сохранено. Если это действительный индекс, вы проверяете свою таблицу. Если у вас есть совпадение, значение было сохранено, в противном случае — нет.

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

person Vaughn Cato    schedule 02.10.2015
comment
Спасибо за ваши мысли. Не могли бы вы взглянуть на мой отредактированный ответ? Я думаю, что использование третьего массива массивов исключает возможность использования потенциально неинициализированной памяти. - person John Cashew; 02.10.2015
comment
@JohnCashew: lazy->used[index] все еще может быть неинициализирован в is_index_used(). - person Vaughn Cato; 02.10.2015
comment
Ты уверен? Я думаю, что use_index() гарантирует, что он инициализирован. - person John Cashew; 02.10.2015
comment
@JohnCashew: хорошо, я понимаю, что вы имеете в виду, но код не кажется правильным. Ожидаете ли вы, что вызывающая сторона use_index() будет передавать последовательные значения для index, начиная с 0? Если это так, то кажется, что обычного массива будет достаточно. Если нет, то проверка is_index_used() кажется неправильной, поскольку она проверяет индекс на соответствие num_used, когда они не связаны. - person Vaughn Cato; 02.10.2015
comment
@vaugncato Спасибо! вы помогли мне обнаружить ошибку в is_index_used(). Пожалуйста, взгляните еще раз. Это исправлено сейчас. И нет необходимости использовать индексы в последовательном порядке. - person John Cashew; 02.10.2015
comment
@JohnCashew: Это выглядит лучше, но теперь вы можете видеть, что если вы передадите произвольный индекс, то lazy->used[index] может быть неинициализирован. Это не ошибка. Это преднамеренный компромисс дизайна с ленивым массивом. - person Vaughn Cato; 02.10.2015
comment
Да, это не баг. Это сделано намеренно. @Вон Катон - person John Cashew; 02.10.2015

Есть много возможных методов. Все зависит от вашей задачи. Например, вы можете запомнить максимальное количество инициализированных элементов max вашего массива. т.е. если ваш алгоритм может гарантировать, что все элементы от 0 до max ara инициализированы, вы можете использовать простую проверку if (0 <= i && i <= max) или что-то в этом роде.

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

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

person Ilya    schedule 02.10.2015