Malloc не выделяет память
Теги: Си память, malloc, calloc, realloc, free, Ошибки выделения памяти, Висячие указатели, Динамические массивы, Многомерные динамические массивы.
Работа с двумерными и многомерными массивами
Д ля динамического создания двумерного массива сначала необходимо создать массив указателей, после чего каждому из элементов этого массива присвоить адрес нового массива.
Для удаления массива необходимо повторить операцию в обратном порядке - удалить сначала подмассивы, а потом и сам массив указателей.
- 1. Создавать массивы "неправильной формы", то есть массив строк, каждая из которых имеет свой размер.
- 2. Работать по отдельности с каждой строкой массива: освобождать память или изменять размер строки.
Создадим "треугольный" массив и заполним его значениями
Чтобы создать трёхмерный массив, по аналогии, необходимо сначала определить указатель на указатель на указатель, после чего выделить память под массив указателей на указатель, после чего проинициализировать каждый из массивов и т.д.
realloc
Е щё одна важная функция – realloc (re-allocation). Она позволяет изменить размер ранее выделенной памяти и получает в качестве аргументов старый указатель и новый размер памяти в байтах:
Функция realloc может как использовать ранее выделенный участок памяти, так и новый. При этом не важно, меньше или больше новый размер – менеджер памяти сам решает, где выделять память.
Пример – пользователь вводит слова. Для начала выделяем под слова массив размером 10. Если пользователь ввёл больше слов, то изменяем его размер, чтобы хватило места. Когда пользователь вводит слово end, прекращаем ввод и выводим на печать все слова.
Хочу обратить внимание, что мы при выделении памяти пишем sizeof(char*), потому что размер указателя на char не равен одному байту, как размер переменной типа char.
Освобождение памяти с помощью free
Т еперь рассмотри, как происходит освобождение памяти. Переменная указатель хранит адрес области памяти, начиная с которого она может им пользоваться. Однако, она не хранит размера этой области. Откуда тогда функция free знает, сколько памяти необходимо освободить?
- 1. Можно создать карту, в которой будет храниться размер выделенного участка. Каждый раз при освобождении памяти компьютер будет обращаться к этим данным и получать нужную информацию.
- 2. Второе решение более распространено. Информация о размере хранится на куче до самих данных. Таким образом, при выделении памяти резервируется места больше и туда записывается информация о выделенном участке. При освобождении памяти функция free "подсматривает", сколько памяти необходимо удалить.
malloc и calloc и vmalloc, ох ты ж!
Многим программистам известен стандартный способ запроса памяти у оперативной системы. В этом механизме задействована функция malloc из стандартной библиотеки С (можете почитать документацию о ней для вашей ОС, введя в мануале в поиск man 3 malloc ). Эта функция берёт один аргумент — количество байтов памяти, которое нужно выделить. Стандартная библиотека С выделяет память по одной из нескольких разных методик, но так или иначе она возвращает указатель на участок памяти, по крайней мере такой же большой, как и запрошенный вами объём.
По умолчанию malloc возвращает неинициализированную память. То есть стандартная библиотека С выделяет какой-то объём и немедленно передаёт его вашей программе, без изменения данных, которые уже там находятся. То есть при использовании malloc вашей программе будет возвращаться буфер, в который она уже записывала данные. Это распространённая причина багов в не безопасных по памяти (memory-unsafe) языках, например С. В целом читать из неинициализированной памяти очень рискованно.
Однако у malloc есть друг, задокументированный на той же странице мануала: calloc . Его главное отличие заключается в том, что он принимает два аргумента — счётчик и размер. Используя malloc , вы просите стандартную библиотеку С: «Пожалуйста, выдели мне не менее n байтов». А при вызове calloc вы просите её: «Пожалуйста, выдели достаточно памяти для n объектов размером m байтов». Очевидно, что первичная идея вызова calloc заключалась в безопасном выделении кучи для массивов объектов 2 .
Но у calloc есть побочный эффект, связанный с его исходным предназначением для размещения массивов в памяти. О нём очень скромно упоминается в мануале.
Это идёт рука об руку с предназначением calloc . К примеру, если вы размещаете в памяти массив значений, то зачастую будет очень полезно, чтобы он изначально имел состояние по умолчанию. В некоторых современных безопасных по памяти языках это уже стало стандартным поведением при создании массивов и структур. Скажем, когда в Go инициализируешь структуру, то все её члены по умолчанию приведены к своим так называемым «нулевым» значениям, эквивалентным «таким значениям, которые были бы, если бы всё сбросили в ноль». Это можно считать обещанием, что все Go-структуры размещаются в памяти с помощью calloc 3 .
Такое поведение означает, что malloc возвращает неинициализированную память, а calloc — инициализированную. А раз так, да ещё и в свете вышеупомянутых строгих обещаний, то операционная система может оптимизировать выделяемую память. И действительно, многие современные ОС так делают.
Конечно, простейший способ внедрить calloc — это написать что-то вроде:
Стоимость подобной функции изменяется примерно линейно по отношению к размеру выделяемой памяти: чем больше байтов, тем дороже их все обнулить. Сегодня большинство ОС по факту содержат стандартные библиотеки С, в которых прописаны оптимизированные пути для memset (обычно используются специализированные процессорные векторные инструкции, позволяющие одной инструкцией обнулять сразу большое количество байтов). Тем не менее стоимость этой процедуры меняется линейно.
Для выделения больших объёмов в ОС используется ещё один трюк, имеющий отношение к виртуальной памяти.
Здесь мы не будем разбирать всю структуру и работу виртуальной памяти, но я очень рекомендую об этом почитать (тема крайне интересная!). Вкратце: виртуальная память — это ложь ядра ОС процессам о доступной памяти. У каждого выполняемого процесса есть своё представление о памяти, принадлежащей ему и только ему. Это представление косвенно «отображается» (mapped) на физическую память.
В результате ОС может прокручивать всевозможные хитрые трюки. Чаще всего она выдаёт за память специальные файлы, отображаемые в неё (memory-mapped file). Они используются для выгрузки содержимого памяти на диск, а также для отображения в них памяти. В последнем случае программа просит ОС: «Пожалуйста, выдели мне n байтов памяти и сохрани их в файле на диске, так, чтобы, когда я буду писать в память, все записи выполнялись бы в этот файл, а когда считываю из памяти, то данные считывались бы из него же».
На уровне ядра это работает так: когда процесс пытается считать из такой памяти, процессор уведомляет, что память не существует, ставит процесс на паузу и бросает «ошибку страницы» (page fault). Ядро помещает в память актуальные данные, чтобы приложение могло их считать. Затем процесс снимается с паузы и находит в соответствующем месте волшебным образом появившиеся данные. С точки зрения процесса всё произошло мгновенно, без паузы.
Этот механизм можно использовать для выполнения других тонких трюков. Один из них заключается в «бесплатности» выделения очень больших объёмов памяти. Или, точнее, в том, чтобы сделать их стоимость пропорциональной степени использования этой памяти, а не выделяемому размеру.
Исторически сложилось так, что многие программы, которым во время выполнения нужны порядочные куски памяти, при запуске создают большой буфер, который потом может распределяться внутри программы в ходе её жизненного цикла. Так делалось потому, что программы писались для окружений, не использовавших виртуальную память; программам приходилось сразу занимать какие-то объёмы памяти, чтобы потом не испытывать в ней недостатка. Но после внедрения виртуальной памяти такое поведение стало ненужным: каждая программа может выделить себе столько памяти, сколько надо, не вырывая у других кусок изо рта 4 .
Чтобы избежать очень больших затрат при запуске приложений, операционные системы начали врать приложениям. В большинстве ОС, если вы попытаетесь выделить более 128 Кб в рамках одного вызова, стандартная библиотека С напрямую попросит у ОС совершенно новые страницы виртуальной памяти, которые покроют запрошенные объёмы. Но главное: такое выделение почти ничего не стоит. Ведь на самом деле ОС ничего не делает: она лишь перенастраивает схему виртуальной памяти. Так что при использовании malloc расходы получаются мизерными.
Память не была «приписана» к процессу, и, как только приложение пытается использовать её на самом деле, возникает ошибка страницы памяти. Здесь вмешивается ОС, находит нужную страницу и помещает её туда, куда обращается процесс, так же, как и в случае с ошибкой памяти и файлом отображаемой памяти. Только разница в том, что виртуальная память обеспечивается памятью физической, а не файлом.
В результате, если вызвать malloc(1024 * 1024 * 1024) для выделения 1 Гб памяти, это произойдёт почти мгновенно, потому что на самом деле процессу память не выделяется. Зато программы могут моментально «выделять» для себя многие гигабайты, хотя в реальности это происходило бы далеко не быстро.
Но ещё удивительнее то, что такая же оптимизация доступна и для calloc . ОС может отображать совершенно новую страницу на так называемую «нулевую страницу»: это страница памяти, доступная только для чтения, причём считываются из неё одни лишь нули. Изначально такое отображение представляет собой копирование при записи (copy-on-write): когда ваш процесс пытается записать данные в эту новую память — вмешивается ядро, копирует все нули в новую страницу, а затем разрешает вам выполнить запись.
Благодаря такому ухищрению со стороны ОС calloc может при выделении больших объёмов делать то же самое, что и malloc , запрашивая новые страницы виртуальной памяти. Это будет происходить бесплатно до тех пор, пока память не начнёт использоваться. Подобная оптимизация означает, что стоимость calloc(1024 * 1024 * 1024, 1) будет равна вызову malloc для такого же объёма памяти, несмотря на то что calloc ещё и обещает заполнять память нулями. Умно!
Итак: если CFFI использовал calloc , то почему память обнулялась?
Для начала: calloc использовался не всегда. Но я подозревал, что в данном случае могу воспроизвести замедление напрямую с помощью calloc , поэтому снова накидал программу:
Очень простая программа на C, выделяющая и освобождающая 100 Мб посредством вызова calloc десять тысяч раз. Затем выполняется выход. Далее — два варианта 5 :
- calloc может использовать вышеописанную хитрость с виртуальной памятью. В таком случае программа должна работать быстро: выделяемая память на самом деле не используется, не разбивается на страницы, а страницы не становятся «грязными» (dirty). ОС врёт нам насчёт выделения, а мы не ловим её за руку, так что всё работает прекрасно.
- calloc может привлечь malloc и вручную обнулить память с помощью memset . Это должно делаться очень-очень медленно: в сумме нам надо обнулить терабайт памяти (десять тысяч циклов по 100 Мб), что очень непросто.
Более того, если увеличить ALLOCATION_SIZE (например, 1000 * 1024 * 1024 ), то на MacOS эта программа станет работать почти мгновенно! Что за чертовщина?
Что тут вообще происходит?
В MacOS есть утилита sample (см. man 1 sample ), которая может многое рассказать о выполняемом процессе, регистрируя его состояние. Для нашего кода sample выдаёт такое:
Здесь мы ясно видим, что куча времени тратится на метод _platform_bzero$VARIANT$Haswell . Он используется для обнуления буферов. То есть MacOS их обнуляет. Почему?
Похоже, вся магия происходит в large_malloc. Эта ветка нужна для выделения памяти крупнее 127 Кб, она использует трюк с виртуальной памятью. Так почему у нас всё медленно работает?
Однако это тот случай для calloc , когда нужно обнулить байты. И если MacOS находит страницу, которую можно использовать повторно и которая была вызвана из calloc , то память обнулится. Вся. И так каждый раз.
В этом есть свой резон: обнулённые страницы — ограниченный ресурс, особенно в условиях скромного железа (смотрю на Apple Watch). Так что если страницу можно использовать повторно, то это может дать хорошую экономию.
Однако кеш страницы полностью лишает нас преимуществ использования calloc для предоставления обнулённых страниц памяти. Это было бы не так уж плохо, если бы делалось только для «грязных» страниц. Если приложение записывает в обнуляемую страницу, то та, вероятно, не будет обнулена. Но MacOS выполняет это безоговорочно. Это значит, что даже если вызвать alloc , free и calloc , вообще не трогая память, то при втором вызове calloc будут взяты страницы, выделенные во время первого вызова и ни разу не поддержанные физической памятью. Поэтому ОС приходится загрузить (page-in) всю эту память, чтобы обнулить её, хотя она уже была обнулена. Этого мы и хотим избежать с помощью средства распределения на базе виртуальной памяти, когда доходит до выделения больших объёмов: ни разу не использовавшаяся память становится использованной «списком свободных» страниц.
В результате на MacOS стоимость calloc линейно возрастает в зависимости от размера выделяемой памяти вплоть до 125 Мб, несмотря на то что другие ОС демонстрируют поведение O(1) начиная со 127 Кб. После 125 Мб MacOS перестаёт кешировать страницы, так что скорость волшебным образом взлетает.
Я не ожидал найти такой баг из программы на Python, и у меня возник ряд вопросов. Например, сколько процессорных циклов теряется на обнуление памяти, которая уже обнулена? Сколько переключений контекста уходит на то, чтобы заставлять приложения загружать (page-in) память, которую они не использовали (и не собираются), чтобы ОС могла бессмысленно её обнулить?
Мне кажется, всё это подтверждает верность старой поговорки: утечки есть во всех абстракциях (all abstractions are leaky). Вы не можете забывать об этом лишь потому, что программируете на Python. Ваша программа выполняется на машине, использующей память и всякие трюки для её управления. Однажды, совершенно неожиданно, написанный вами код станет работать очень медленно. И разобраться с этим удастся, лишь разобравшись во внутренностях ОС.
Этот баг был описан как Radar 29508271. Один из самых странных багов, что мне встречались.
Ошибка при выделении памяти с помощью malloc
Помогите выдает ошибку в рядочке :"Ar = ( far *)malloc(size*sizeof(int));" полный текст програмы.
При выделении памяти через malloc, как создавать объекты ?
Выделяю память через malloc под 4 объекта, как их создать ? myClass * ptr = (myClass*).
Почему не вылетает ошибка при выделении памяти под динамический массив, размером 100 Гб?
Здравствуйте, знатоки! Столкнулся с проблемой выделения памяти под динамические массивы. При.
Enum и типы данных. Как задать тип значений явно, и какой тип будет при переполнении?
Пытаюсь сделать функцию с передачей нескольких параметров,используя битовые операции. В качестве.
malloc returns a void pointer (void *), which indicates that it is a pointer to a region of unknown data type. The use of casting is required in C++ due to the strong type system, whereas this is not the case in C1. The lack of a specific pointer type returned from malloc is type-unsafe behavior according to some programmers: malloc allocates based on byte count but not on type. This is different from the C++ new operator that returns a pointer whose type relies on the operand.
Ну да. А указатель на void можно привести к указателю на любой тип без потери данных.
Собственно из за этого он нормально присваивается указателю на char в примере
Отсюда и вопрос.
Решение
Все правильно. Выполнять явное приведение типа malloc в С - не нужно и это дурная практика. Идиома элегантного использования malloc в С такова
Обратите внимание: не только нет приведения типа, но и под sizeof имя типа не упоминается. Постоянное приведение результата malloc в С - индикатор низкокачественного кода.
Манера приводить результат malloc тянется из старинных времен, когда в С не было типа void * и malloc возвращал char * . В современном коде единственное оправдание такого приведения - это некий кросс-компилируемый код, который по какой-то причине должен компилироваться и как С, и как С++.
С этой манерой борются уже давно - в любом FAQ вы прочитаете, что ни в коем случае не следует приводить результат malloc . Но количество учебных пособий, просто тупо перепечатывающих устаревшие примеры из эпохи динозавров, до сих пор велико, и дурная привычка продолжает жить.
С этой манерой борются уже давно - в любом FAQ вы прочитаете, что ни в коем случае не следует приводить результат malloc. Но количество учебных пособий, просто тупо перепечатывающих устаревшие примеры из эпохи динозавров, до сих пор велико, и дурная привычка продолжает жить.
Зачем и откуда такая агрессивность? Да, если подключать правильные хедеры, то приведение маллока излишне. Но и вреда никакого не несет. Не считая лишнего десятка букв в исходном коде. И почему "ни в коем случае"? Что, это кому-то может нанести вред?
А излишеств в нашей профессии навалом. И если с каждым бороться с пеной у рта, то никаких желез внутренней секреции не хватит
Добавлено через 1 минуту
TheCalligrapher,
Спасибо!
Может не по теме, но тут вы привели интересный пример Создание динамического двухмерного массива с фразой
Но код не понимаю. Может у меня не правильное представление о том как многомерные массивы хранятся в памяти.
Можете объяснить, если не трудно, как это работает. Не хочется в таких случаях лишний раз вызывать аллокатор для каждой строки
Как раз несет. Если не подключен стдлиб, то без каста компилятор выдаст предупреждение и косяк будет замечен. А явный каст его подавит. А это трындец.
Ключевой момент тут в том, что динамический двухмерный массив, выделяемый по приведенной ссылке (как и все остальные динамические массивы в той теме), по своей физической структуре не имеет ничего общего (. ) со встроенным двумерным массивом типа
Это две структуры данных обладают одинаковым внешним интерфейсом, т.е. доступ к элементу массива делается через синтаксис a[i][j] , но это - лишь внешнее, ничего не значащее сходство ("синтаксический сахар")
Я по вашему комментарию догадываюсь, то вы, посмотрев на код по ссылке, пытаетесь применять эти сведения к обычным языковым двухмерным массивам
и это вводит вас в заблуждение. Не надо этого делать. Языковый массив устроен по-другому.
По ссылке разными способами выделяются динамические двухмерные массивы, которые реализованы как массивы указателей на начала одномерных подмассивов. Встроенные же языковые двухмерные массивы - это просто массивы массивов, без каких-либо указателей вообще.
Там сразу выделяется память на всю матрицу. Собственно, моделируется работа транслятора, когда он выделяет память для массива A[Row][Col]. Лично я не стал бы советовать этот прием новичкам. Нужно очень хорошо представлять себе устройство памяти и механизмы ее выделения. А результат - тот же. Хотя и красиво (для тех, кто понимает). К сожалению, этот прием не проходит, если строки массива разной длины.
А вам, sys_beginner, могу посоветовать не лезть пока в эти тонкости. Вот набьете руку и шишек, тогда к этому можно будет и вернуться.
Если не подключен стдлиб, то без каста компилятор выдаст предупреждение и косяк будет замечен. А явный каст его подавит.
Совершенно верно. То есть идея совершенно тривиальная: если вы заранее знаете, что вам сейчас надо будет 50 раз вызывать malloc(10) , то вы можете вместо этого просто-напросто один раз вызвать malloc(500) , а потом просто "раздать по 10" всем 50 получателям.
то получу такое-же предупреждение (если не ошибку) и тут же кинусь исправляться.
Ну а в первом варианте что может плохого? ума не приложу.
Причем эти варианты одинаково (и, надеюсь, надежно) работают и при подключенном хедере, и без него.
Есть такое. Когда вызывал malloc без stdlib получал ошибку. Потом встретил вариант с кастом, пришел сюда
TheCalligrapher,
Спасибо за уточнение. В принципе думаю разобрался что к чему.
Строка 3: выделяется память под указатели на строки матрицы
Строка 4: выделяется память под указатели на элементы строк матрицы
Строка 6: каждому указателю на строку матрицы присваивается память её ячеек
Если я все правильно понял, то в чем заключается гарантия того, что занимая память таким макаром мы не перезаписываем ту область памяти, которая уже занята? Сначала думал, что можно гарантировать это тем, что данные в стеке занимают память последовательно, но потом подумал, что переменные цикла for могут находиться именно в той области памяти куда пишутся данные матрицы. Или у цикла в данном случае своя стековая область, отличная от стековой области функции? В случае объявления двумерного массива с помощью конструкции [][] вопросов не возникает, думаю этот вопрос решает сам компилятор. А в этом случае имитируется его работа.
Байт,
А мне интересно Думаю уже близок к истине
Такого приема я не знал. Приму на вооружение, если позволите
Маленькая деталька. Кто будет память освобождать? Сам allocMatrix? Как будто ему это не к чему (работать-то с массивом будет вызывающая программа. Тогда она должна видеть data. Но это, конечно, можно обойти. Правда, достаточно уклюжего способа в глаза не бросилось.
Но есть еще тонкие вопросы.
Чуток погодим.
Речь идет о предупреждении "warning: initialization makes pointer from integer without a cast". При наличии каста, такого предупреждения не будет.
Речь идет о хрестоматийном примере:
Вася Пупкин написал у себя в программе
но забыл подключить
Компилятор честно принимал из функции int и конвертировал его к типу int * . И это даже работало, ибо Вася Пупкин сидел на 32-битной платформе, на которой размер int совпадает с размером int * (оба - 4 байта) и соглашение о возвращении значений int и int * из функций тоже совпадает (скажем, возвращаются в 32-битном регистре eax ). То есть в указатель p ложилось правильное значение возвращенное malloc .
Однако в один прекрасный день Вася Пупкин перешел на 64-битную платформу. На этой платформе тип int был по прежнему 32-битным, а вот тип int * был уже 64-битным. Код по-прежнему прекрасно компилировался, но при выполнении - падал. Почему? А потому, что на 64-битную платформе указатели возвращаются из функции несколько по-другому (скажем, в 64-битном регистре rax ). Компилятор же полагал, что malloc возвращает int (в 32-битном регистре eax ), то есть принимал во внимание лишь часть возвращенного malloc значения, конвертировал его в тип int * (дополняя нулями) и клал результат в p . В p , разумеется, получалась белиберда.
подумал, что переменные цикла for могут находиться именно в той области памяти куда пишутся данные матрицы.
Скорее всего ошибаюсь. Вся необходимая память была выделена до описания цикла. Но гарантируют ли компиляторы последовательную резервацию памяти в стеке?
Ошибка при выделении памяти
Вопрос: из-за чего программа может рушится? int *NRRow = new int ; int *NRow = new int ; int.
Ошибка при выделении памяти
Здравствуйте, друзья. Подскажите, пожалуйста, почему выскакивает ошибка при повторном выделении.
Ошибка при выделении памяти
что-то не так с выделением памяти, после запуска выдает ошибку, не могу понять где налажал.
Ошибки при выделении памяти
Добрый вечер. В коде я например выделял динамическую память. char *c=new char ; Но не.
Различные аргументы realloc и malloc.
При вызове функции malloc, realloc и calloc с нулевым размером поведение не определено. Это значит, что может быть возвращён как NULL, так и реальный адрес. Им можно пользоваться, но к нему нельзя применять операцию разадресации.
Вызов realloc(NULL, size_t) эквиваленте вызову malloc(size_t).
Однако, вызов realloc(NULL, 0) не эквивалентен вызову malloc(0) :) Понимайте это, как хотите.
calloc
Ф ункция calloc выделяет n объектов размером m и заполняет их нулями. Обычно она используется для выделения памяти под массивы. Синтаксис
Примеры
1. Простое скользящее среднее равно среднему арифметическому функции за период n. Пусть у нас имеется ряд измерений значения функции. Часто эти измерения из-за погрешности "плавают" или на них присутствуют высокочастотные колебания. Мы хотим сгладить ряд, для того, чтобы избавиться от этих помех, или для того, чтобы выявить общий тренд. Самый простой способ: взять n элементов ряда и получить их среднее арифметическое. n в данном случае - это период простого скользящего среднего. Так как мы берём n элементов для нахождения среднего, то в результирующем массиве будет на n чисел меньше.
Это простой пример. Большая его часть связана со считыванием данных, вычисление среднего всего в девяти строчках.
2. Сортировка двумерного массива. Самый простой способ сортировки - перевести двумерный массив MxN в одномерный размером M*N, после чего отсортировать одномерный массив, а затем заполнить двумерный массив отсортированными данными. Чтобы не тратить место под новый массив, мы поступим по-другому: если проходить по всем элементам массива k от 0 до M*N, то индексы текущего элемента можно найти следующим образом:
j = k / N;
i = k - j*M;
Заполним массив случайными числами и отсортируем
3. Бином Ньютона. Создадим треугольную матрицу и заполним биномиальными коэффициентами
Если Вы желаете изучать этот материал с преподавателем, советую обратиться к репетитору по информатике
Всё ещё не понятно? – пиши вопросы на ящик
Нет, здесь не будет ничего из серии «Аааа, я сделал malloc (new), и забыл сделать free (delete)!»
Здесь будет нечто изощренное: мы будем отрезать кусочки памяти по чуть-чуть, прятать их в укромное место… А когда операционная система заплатит выкуп скажет «Хватит!», мы попробуем вернуть все обратно. Казалось бы, простейшая операция выделения и освобождения памяти — ничего не предвещает беды.
Тем кому интересно как уничтожить забить память — прошу под хабракат
Немножко предыстории
По долгу службы приходится много работать с большими буфферами памяти (представьте себе изображение 5000x40000 пиксел). Порой (из-за фрагментации) не получается выделять непрерывный кусок памяти для всего. Поэтому был написан некоторый менеджер памяти, который выделял сколько есть, возможно, несколькими кусками. Естественно, менеджер памяти должен как выделять, так и удалять. Тогда была обнаружена следующая интересная вещь: Task Manager после освобождения показывает уровень использования памяти такой же как и до выделения блока. Однако никакой новый блок памяти в программе не может быть выделен. Использование средств анализа виртуальной памяти (VMMap от Марка Русиновича) показывает, что память остается занята несмотря на ее освобождение в коде и несмотря на показания TM.
Анализ
Напишем быстренько какую-нибудь программку, которая выделяет и освобождает память. Что нибудь такое, сродни «Hello, World!»:
Несложными подсчетами можно убедиться, что программа должна выделить 1 ГБ памяти, а затем все освободить. После запуска и проверки вся память освобождается. Хм, кажется, система шантажу не поддается. Впрочем, мы резали большие куски.
Теперь возьмем и немножко поправим исходный код:
В этом случае мы получим, что память выделилась, но не освободилась:
До «Memory freed»:
После «Memory freed»:
- 520168 байт и выше — освобождение проходит нормально
- 520167 байт и ниже — имеем описанную проблему
Возможное объяснение
После длительных бдений за гуглом и изучения форумов я пришел к следующим выводам.
Оказывается что после выделения памяти с помощью функций malloc/new в том случае если выделяется маленький кусок, то память не освобождается функциями free/delete, а переходит из разряда committed в разряд reserved. И если мы обращаемся к данной памяти тут же после удаления (по всей видимости в рамках одного хипа), то она может быть выделена повторно. Однако при попытке выделить память из другого класса (либо статической функции) мы получим исключение — не достаточно памяти. По всей видимости при выделении памяти из статической функции память выделяется не в том же хипе, что и при обычном выделении изнутри класса приложения.
В результате после создания большого блока памяти (из маленьких кусочков) мы исчерпываем память и не можем в дальнейшем выделить себе еще немножко ну хоть чуть-чуть! памяти.
Неправильное решение
Использование функций VirtualAlloc/VirtualFree (MSDN) решает данную проблему, память полностью возвращается процессу после использования (ключ MEM_RELEASE), однако при использовании VirtualAlloc происходит сильная фрагментация памяти, и где-то 800Мб памяти не доступно для использования, т.к. максимальный размер свободного блока — 28Кб. Классический malloc в этом плане работает лучше, т.к. там есть некоторый дефрагментатор.
Окончательное решение
Нашел стороннюю реализацию malloc и free (как выясняется, широко известную в узких кругах), которая имеет классический недостаток дефрагментации памяти, но в месте с тем освобождает полностью память после использования. Плюс еще и заметно быстрее работает.
Для любопытствующих и жаждущих имеется ссылка
Ремарки
Под ОС *NIX (Ubuntu, Debian, CentOS) повторить проблему не удалось)
Под ОС Windows проблема была воспроизведена на Windows Server 2003 x64, Windows 7 x64, Windows XP x32.
Не стоит прямо так сразу доверять давно проверенным функциям, в них может крыться подвох.
При попытке выделить память указателю в процедуре, память не выделяется в main, и происходит ошибка при попытке изменеyии данных массива (вылет программы).
Уже откопал на форуме решение, заключающее в использовании указателя на указатель в процедуре, т.е. getinput(int **arg).
Вот оно Тык
Хотелось бы узнать, в чём "физическая" природа такого феномена, почему C не хочет выделить память по переданному указателю, а требует использование двойного указателя в качестве аргумента и дополнительного локального указателя.
Сама ошибка во вложении
Почему не работает malloc()?
int *p; p=malloc(10*sizeof(int)); При компиляции указывает на строчку с malloc и пишет error.
Указатели. Объясните почему и как работает malloc ?
Юзер вводит слово char name; scanf("%s", name); Необходимо сделать массив такого же размера.
Почему malloc работает не так, как ожидается?
int main() < int* p =malloc(sizeof(int)); p=5; printf("%s\n",strerror(errno)); .
Глобальные и локальные переменные, одна и та же прога в процедуре Не работает, а просто так работает. Почему?
Здравствуйте, есть программка которая переводит из 2-ной системы в 10-ную. Она работает. А вот.
Ну с функцией оно работает, спору нету, и выглядит логичней. Но в большой программе (этот пример я привёл чисто как сферический в вакууме) у меня в вызове процедуры изменяются два элемента, и красивей (а нас заставляют молится на красоту кода в универе) использовать процедуру. Вызывать же отдельно две функции не позволяет совесть, эти 2 элемента очень тесно связаны.
Насчёт почитать про передачу параметров.
Почитал статьи, понял следующее: в процедуру мы передаём пустой указатель. Затем в процедуре мы выделяем память для этого указателя. Вопрос, а не выделяем ли мы память, которая доступна только этой процедуре, т.е локальную память? Тогда логично, что после завершении процедуры ссылка на область памяти будет указывать на непойми что. Если это так, тогда почему работает функция. Ведь там мы тоже выделяем локальную память и возвращаем этот же указатель на непойми что.
Всё началось, как и многие другие расследования, с баг-репорта.
Это просто замечательный воспроизводимый сценарий, потому что он совершенно чётко указывает на стек Requests. Здесь не выполняется пользовательский (user-supplied) код: это часть библиотеки Requests или одной из его зависимостей; нет вероятности, что это пользователь написал дурацкий низкопроизводительный код. Настоящая фантастика. Ещё более фантастично использование публичного URL. Я мог выполнить сценарий! И, сделав это, я столкнулся с багом. При каждом выполнении.
Здесь была ещё одна прелестная подробность:
При 10 Мб не отмечается какого-то роста нагрузки на процессор и влияния на пропускную способность. При 1 Гб процессор загружается на 100 %, как и при 100 Мб, но пропускная способность падает ниже 100 Кб/с, в отличие от 1 Мб/с при 100 Мб.
Это очень интересный момент: он намекает на то, что литеральное значение (literal value) размера чанка влияет на рабочую нагрузку. Если учесть, что это происходит только при использовании PyOpenSSL, а также что большую часть времени стек обслуживает вышеприведённый код, проблема становится ясна:
Расследование показало, что стандартное поведение CFFI относительно FFI.new заключается в возвращении обнулённой памяти. Это означало линейное увеличение избыточности в зависимости от размера выделяемой памяти: более крупные объёмы приходилось обнулять дольше. Следовательно, плохое поведение связано с выделением больших объёмов. Мы воспользовались возможностью CFFI отключить обнуление этих буферов, и проблема ушла 1 . Так она решена, верно?
Шутки в сторону: это действительно позволило решить проблему. Но несколько дней спустя мне задали очень глубокомысленный вопрос: зачем вообще память активно обнулялась? Чтобы понять суть вопроса, давайте отвлечёмся и поговорим о выделении памяти в POSIX-системах.
malloc
В предыдущей главе уже обсуждалось, что локальные переменные кладутся на стек и существую до тех пор, пока мы не вышли из функции. С одной стороны, это позволяет автоматически очищать память, с другой стороны, существует необходимость в переменных, время жизни которых мы можем контролировать самостоятельно. Кроме того, нам необходимо динамическое выделение памяти, когда размер используемого пространства заранее не известен. Для этого используется выделение памяти на куче. Недостатков у такого подхода два: во-первых, память необходимо вручную очищать, во-вторых, выдеение памяти – достаточно дорогостоящая операция.
Для выделения памяти на куче в си используется функция malloc (memory allocation) из библиотеки stdlib.h
Функция выделяет size байтов памяти и возвращает указатель на неё. Если память выделить не удалось, то функция возвращает NULL. Так как malloc возвращает указатель типа void, то его необходимо явно приводить к нужному нам типу. Например, создадим указатель, после этого выделим память размером в 100 байт.
После того, как мы поработали с памятью, необходимо освободить память функцией free.
Используя указатель, можно работать с выделенной памятью как с массивом. Пример: пользователь вводит число – размер массива, создаём массив этого размера и заполняем его квадратами чисел по порядку. После этого выводим и удаляем массив.
Здесь (int *) – приведение типов. Пишем такой же тип, как и у указателя.
size * sizeof(int) – сколько байт выделить. sizeof(int) – размер одного элемента массива.
После этого работаем с указателем точно также, как и с массивом. В конце не забываем удалять выделенную память.
Теперь представим на рисунке, что у нас происходило. Пусть мы ввели число 5.
Функция malloc выделила память на куче по определённому адресу, после чего вернула его. Теперь указатель p хранит этот адрес и может им пользоваться для работы. В принципе, он может пользоваться и любым другим адресом.
Когда функция malloc "выделяет память", то она резервирует место на куче и возвращает адрес этого участка. У нас будет гарантия, что компьютер не отдаст нашу память кому-то ещё. Когда мы вызываем функцию free, то мы освобождаем память, то есть говорим компьютеру, что эта память может быть использована кем-то другим. Он может использовать нашу память, а может и нет, но теперь у нас уже нет гарантии, что эта память наша. При этом сама переменная не зануляется, она продолжает хранить адрес, которым ранее пользовалась.
Это очень похоже на съём номера в отеле. Мы получаем дубликат ключа от номера, живём в нём, а потом сдаём комнату обратно. Но дубликат ключа у нас остаётся. Всегда можно зайти в этот номер, но в нём уже кто-то может жить. Так что наша обязанность – удалить дубликат.
Иногда думают, что происходит "создание" или "удаление" памяти. На самом деле происходит только перераспределение ресурсов.
Ошибки при выделении памяти
1. Бывает ситуация, при которой память не может быть выделена. В этом случае функция malloc (и calloc) возвращает NULL. Поэтому, перед выделением памяти необходимо обнулить указатель, а после выделения проверить, не равен ли он NULL. Так же ведёт себя и realloc. Когда мы используем функцию free проверять на NULL нет необходимости, так как согласно документации free(NULL) не производит никаких действий. Применительно к последнему примеру:
Хотелось бы добавить, что ошибки выделения памяти могут случиться, и просто выходить из приложения и выкидывать ошибку плохо. Решение зависит от ситуации. Например, если не хватает памяти, то можно подождать некоторое время и после этого опять попытаться выделить память, или использовать для временного хранения файл и переместить туда часть объектов. Или выполнить очистку, сократив используемую память и удалив ненужные объекты.
2. Изменение указателя, который хранит адрес выделенной области памяти. Как уже упоминалось выше, в выделенной области хранятся данные об объекте - его размер. При удалении free получает эту информацию. Однако, если мы изменили указатель, то удаление приведёт к ошибке, например
Таким образом, если указатель хранит адрес, то его не нужно изменять. Для работы лучше создать дополнительную переменную указатель, с которой работать дальше.
3. Использование освобождённой области. Почему это работает в си, описано выше. Эта ошибка выливается в другую – так называемые висячие указатели (dangling pointers или wild pointers). Вы удаляете объект, но при этом забываете изменить значение указателя на NULL. В итоге, он хранит адрес области памяти, которой уже нельзя воспользоваться, при этом проверить, валидная эта область или нет, у нас нет возможности.
Эта программа отработает и выведет мусор, или не мусор, или не выведет. Поведение не определено.
Если же мы напишем
то программа выкинет исключение. Это определённо лучше, чем неопределённое поведение. Если вы освобождаете память и используете указатель в дальнейшем, то обязательно обнулите его.
4. Освобождение освобождённой памяти. Пример
Здесь дважды вызывается free для переменной a. При этом, переменная a продолжает хранить адрес, который может далее быть передан кому-нибудь для использования. Решение здесь такое же как и раньше - обнулить указатель явно после удаления:
5. Одновременная работа с двумя указателями на одну область памяти. Пусть, например, у нас два указателя p1 и p2. Если под первый указатель была выделена память, то второй указатель может запросто скомпрометировать эту область:
Рассмотрим код ещё раз.
Теперь оба указателя хранят один адрес.
А вот здесь происходит непредвиденное. Мы решили выделить под p2 новый участок памяти. realloc гарантирует сохранение контента, но вот сам указатель p1 может перестать быть валидным. Есть разные ситуации. Во-первых, вызов malloc мог выделить много памяти, часть которой не используется. После вызова ничего не поменяется и p1 продолжит оставаться валидным. Если же потребовалось перемещение объекта, то p1 может указывать на невалидный адрес (именно это с большой вероятностью и произойдёт в нашем случае). Тогда p1 выведет мусор (или же произойдёт ошибка, если p1 полезет в недоступную память), в то время как p2 выведет старое содержимое p1. В этом случае поведение не определено.
Два указателя на одну область памяти это вообще-то не ошибка. Бывают ситуации, когда без них не обойтись. Но это очередное минное поле для программиста.
Читайте также: