Я был назначен на проект, который представляет собой сложную унаследованную систему, написанную на C ++ и ActiveX ~ 10 лет.
Установка — Microsoft Visual Studio 2008.
Хотя в настоящее время нет проблем с системой, в рамках проверки безопасности унаследованной системы средство автоматического сканирования кода безопасности отметило случаи realloc как проблему плохой практики из-за уязвимости безопасности.
Это связано с тем, что функция realloc может оставить копию конфиденциальной информации в памяти, где ее нельзя перезаписать. Инструмент рекомендует заменить realloc на malloc, memcpy и free.
Теперь функция realloc, будучи универсальной, будет выделять память, когда исходный буфер равен нулю. Это также освобождает память, когда размер буфера равен 0. Я смог проверить оба этих сценария.
Источник: MDSN Library 2001
realloc возвращает пустой указатель на перераспределенный (и, возможно, перемещенный) блок памяти. Возвращаемое значение равно NULL, если размер равен нулю, а аргумент буфера не равен NULL, или если недостаточно памяти для расширения блока до заданного размера. В первом случае исходный блок освобождается. Во втором исходный блок остается неизменным. Возвращаемое значение указывает на область памяти, которая гарантированно будет соответствующим образом выровнена для хранения любого типа объекта. Чтобы получить указатель на тип, отличный от void, используйте приведение типа к возвращаемому значению.
Итак, моя функция замены, которая использует malloc, memcpy и free, должна обслуживать эти случаи.
Ниже я воспроизвел исходный фрагмент кода (реализация массива), который использует realloc для динамического изменения размера и сокращения своего внутреннего буфера.
Сначала определение класса:
template <class ITEM>
class CArray
{
// Data members:
protected:
ITEM *pList;
int iAllocUnit;
int iAlloc;
int iCount;
public:
CArray() : iAllocUnit(30), iAlloc(0), iCount(0), pList(NULL)
{
}
virtual ~CArray()
{
Clear(); //Invokes SetCount(0) which destructs objects and then calls ReAlloc
}
Существующий метод ReAlloc:
void ReAllocOld()
{
int iOldAlloc = iAlloc;
// work out new size
if (iCount == 0)
iAlloc = 0;
else
iAlloc = ((int)((float)iCount / (float)iAllocUnit) + 1) * iAllocUnit;
// reallocate
if (iOldAlloc != iAlloc)
{
pList = (ITEM *)realloc(pList, sizeof(ITEM) * iAlloc);
}
}
Ниже приведена моя реализация, которая заменяет их на malloc, memcpy и free:
void ReAllocNew()
{
int iOldAlloc = iAlloc;
// work out new size
if (iCount == 0)
iAlloc = 0;
else
iAlloc = ((int)((float)iCount / (float)iAllocUnit) + 1) * iAllocUnit;
// reallocate
if (iOldAlloc != iAlloc)
{
size_t iAllocSize = sizeof(ITEM) * iAlloc;
if(iAllocSize == 0)
{
free(pList); /* Free original buffer and return */
}
else
{
ITEM *tempList = (ITEM *) malloc(iAllocSize); /* Allocate temporary buffer */
if (tempList == NULL) /* Memory allocation failed, throw error */
{
free(pList);
ATLTRACE(_T("(CArray: Memory could not allocated. malloc failed.) "));
throw CAtlException(E_OUTOFMEMORY);
}
if(pList == NULL) /* This is the first request to allocate memory to pList */
{
pList = tempList; /* assign newly allocated buffer to pList and return */
}
else
{
size_t iOldAllocSize = sizeof(ITEM) * iOldAlloc; /* Allocation size before this request */
size_t iMemCpySize = min(iOldAllocSize, iAllocSize); /* Allocation size for current request */
if(iMemCpySize > 0)
{
/* MemCpy only upto the smaller of the sizes, since this could be request to shrink or grow */
/* If this is a request to grow, copying iAllocSize will result in an access violation */
/* If this is a request to shrink, copying iOldAllocSize will result in an access violation */
memcpy(tempList, pList, iMemCpySize); /* MemCpy returns tempList as return value, thus can be omitted */
free(pList); /* Free old buffer */
pList = tempList; /* Assign newly allocated buffer and return */
}
}
}
}
}
Заметки:
Объекты построены и уничтожены правильно как в старом, так и в новом коде.
Утечки памяти не обнаружены (как сообщает Visual Studio, встроенная в функции кучи отладки CRT: http://msdn.microsoft.com/en-us/library/e5ewb1h3(v=vs.90).aspx)
Я написал небольшой тестовый жгут (консольное приложение), который выполняет следующие действия:
а. Добавьте 500000 экземпляров класса, содержащего 2 целых числа и строку STL.
Добавленные целые числа работают счетчик и его строковые представления, например, так:
for(int i = 0; i < cItemsToAdd; i++)
{
ostringstream str;
str << "x=" << 1+i << "\ty=" << cItemsToAdd-i << endl;
TestArray value(1+i, cItemsToAdd-i, str.str());
array.Append(&value);
}
б. Откройте большой файл журнала, содержащий 86526 строк различной длины, добавив к экземпляру этого массива: CArray of CStrings и CArray из строк.
Я провел тестовый жгут с использованием существующего метода (базовый уровень) и моего модифицированного метода. Я запускал его как в отладочной, так и в выпускной сборках.
Тест-1: Отладка сборки -> Добавление класса с int, int, string, 100000 экземпляров:
Исходная реализация: 5 секунд, модифицированная реализация: 12 секунд
Тест-2: Отладка сборки -> Добавление класса с int, int, string, 500000 экземпляров:
Первоначальная реализация: 71 секунда, модифицированная реализация: 332 секунды
Тест-3: Выпуск сборки -> Добавление класса с int, int, string, 100000 экземпляров:
Исходная реализация: 2 секунды, модифицированная реализация: 7 секунд
Тест-4: Выпуск сборки -> Добавление класса с int, int, string, 500000 экземпляров:
Первоначальная реализация: 54 секунды, модифицированная реализация: 183 секунды
Тест-5: Отладка сборки -> Чтение большого файла журнала с 86527 строками CArray of CString
Исходная реализация: 5 секунд, модифицированная реализация: 5 секунд
Тест-6: Выпуск сборки -> чтение большого файла журнала с 86527 строками CArray of CString
Исходная реализация: 5 секунд, модифицированная реализация: 5 секунд
Тест-7: Отладка сборки -> Чтение большого файла журнала с 86527 строками.
Исходная реализация: 12 секунд, модифицированная реализация: 16 секунд
Тест-8: Выпуск сборки -> чтение большого файла журнала с 86527 строками CArray of string
Исходная реализация: 9 секунд, модифицированная реализация: 13 секунд
Как видно из приведенных выше тестов, realloc всегда быстрее по сравнению с memalloc, memcpy и free. В некоторых случаях (например, Test-2) это быстрее на целых 367%. Аналогично для Теста-4 это 234%. Итак, что я могу сделать, чтобы получить эти цифры, которые сопоставимы с реализацией realloc?
Можно ли сделать мою версию более эффективной?
Обратите внимание, что я не могу использовать C ++ new и delete. Я должен использовать только malloc и бесплатно. Я также не могу изменить ни один из других методов (поскольку это существующая функциональность), и последствия огромны. Так что мои руки связаны, чтобы получить лучшую реализацию realloc, которую я, возможно, могу.
Я проверил, что моя измененная реализация является функционально правильной.
PS: Это мой первый ТАК. Я старался быть максимально подробным. Предложения по публикации также приветствуется.
Прежде всего, я хотел бы отметить, что вы не обращаетесь к уязвимости, поскольку освобождаемая память также не очищается, так же как и realloc.
Также обратите внимание, что ваш код делает больше, чем старый realloc: он выдает исключение, когда не хватает памяти. Что может быть бесполезно.
Почему ваш код медленнее, чем realloc? Вероятно, потому, что realloc использует скрытые ярлыки, которые вам недоступны. Например, realloc может выделять больше памяти, чем вы фактически запрашиваете, или выделять непрерывную память сразу после окончания предыдущего блока, поэтому ваш код выполняет больше memcpy, чем realloc.
Дело в случае. Выполнение следующего кода в CompileOnline дает результат Wow no copy
#include <iostream>
#include <stdlib.h>
using namespace std;
int main()
{
void* p = realloc(NULL, 1024);
void* t = realloc(p, 2048);
if (p == t)
cout << "Wow no copy" << endl;
else
cout << "Alas, a copy" << endl;
return 0;
}
Что вы можете сделать, чтобы сделать ваш код быстрее?
Вы можете попытаться выделить больше памяти после выделенного блока, но тогда освобождение памяти становится более проблематичным, так как вам нужно запомнить все указатели, которые вы выделили, или найти способ изменить таблицы поиска, используемые free, чтобы освободить правильный объем памяти за один раз.
ИЛИ ЖЕ
Используйте общую стратегию (внутреннего) выделения памяти в два раза больше, чем было выделено ранее, и (необязательно) сокращения памяти только тогда, когда новый порог составляет менее половины выделенной памяти.
Это дает вам немного свободного пространства, поэтому не каждый раз, когда увеличивается объем памяти, необходимо вызывать malloc / memcpy / free.
Если вы посмотрите на реализацию realloc, например.
http://www.scs.stanford.edu/histar/src/pkg/uclibc/libc/stdlib/malloc/realloc.c
вы видите, что разница между вашей реализацией и существующей
является то, что он расширяет блок кучи памяти вместо создания нового блока
используя низкоуровневые звонки. Это, вероятно, объясняет разницу в скорости.
Я думаю, что вам также нужно учитывать значение memset памяти каждый раз, когда вы делаете realloc, потому что тогда снижение производительности кажется неизбежным.
Я считаю, что аргумент о том, что realloc оставляет код в памяти, слишком параноидален, потому что то же самое можно сказать и о обычном malloc / calloc / free. Это будет означать, что вам нужно будет не только найти все reallocs / malloc / callocs, но и любую функцию времени выполнения или стороннюю функцию, которая использует эти функции внутри себя, чтобы быть уверенным, что ничего не хранится в памяти, в противном случае другой способ — создать собственную кучу. и замените его на обычный, чтобы сохранить его в чистоте.
Концептуально realloc () не делает ничего слишком умного — он распределяет память на несколько блоков точно так же, как вы делаете это в ReAllocNew.
Единственная концептуальная разница может заключаться в способе расчета размера нового блока.
realloc может использовать что-то вроде этого:
int new_buffer_size = old_buffer_size * 2;
и это уменьшит количество перемещений памяти по сравнению с тем, что у вас есть.
В любом случае, я думаю, что формула расчета размера блока является ключевым фактором.