首先,这个问题不适用于那些认为自己是C ++警察的官员的人,因为它涉及一些严重的C弯曲以挤回一点记忆所以请用你的治安警察帽子来阅读这个问题。
我有一个程序,其中包含许多用 malloc 分配的字符串,其中大多数字符串都是1个字符长。 包含1个字符串的字符串需要大约32个字节的实内存,包括描述符,块大小等。 所以我的狡猾计划是使用char *指针来存储char或字符串:
char *sourceStringOrChar;
sourceStringOrChar = malloc(10);
//-- OR
sourceStringOrChar = (char *)'A'; //-- Note that its '' and not ""
if((intptr_t)sourceStringOrChar & (~0xFF))
TRACE("%s\n", sourceStringOrChar);
else
TRACE("%c\n", sourceStringOrChar);
我已经知道malloc在内存不足时返回 ENOMEM ,其中此常量的实际值为12.这让我有一些希望,即malloc返回的结果存在空缺。 我已经开始阅读 malloc 的源代码来确定这是否可行,但如果有人对 malloc 的深层有所了解,那么可能给我节省了很多时间。
编辑:
有些人担心免费/ strlen等,但请注意,这是一个示例代码,可以使用
进行与上述相同的处理if((intptr_t)sourceStringOrChar & (~0xFF))
{
strlen(sourceStringOrChar);
free(sourceStringOrChar);
}
此外,如果没有大记忆问题,我也不会走这条危险的道路
答案 0 :(得分:0)
我一直在研究CLISP的代码。在那里做了类似的事情来将一些立即值压缩到指针中(而不是从垃圾收集堆中分配对象)。
在CLISP的情况下,这是有效的,因为我们知道分配器可以返回的地址范围。然后有一些永远不会被有效地址设置,如果设置表明“指针”不是实际指针而是数据(在你的情况下是单个或多个字符)。
顺便说一句:使用char *
并不是一个好计划,由于未定义的行为或只是意外地将这样的“指针”传递给例如strlen
,你只能在脚下射击一半。我想使用union
是一种更好的方法(虽然我现在没有时间检查标准中关于是否允许以这种方式使用联合的实际规则)。更安全的是在结构中打包uintptr_t
。
[..]但如果有人对malloc的深层知识有所了解,可能会为我节省很多时间。
那不会给你买任何东西。切换操作系统,平台或仅使用标准库,一切都可能与您目前正在查看的malloc实现的内容不同。
一种方法是使用您自己的分配器 - 就像使用CLISP一样 - 从一些池中获取内存(例如在Linux / BSD上使用mmap
获取),该池位于某个预定义的地址。
但您也可以使用malloc
(或更合适的功能,如C11 aligned_alloc
或posix_memalign
)来为allocate aligned memory添加字符串。假设您将每个字符串对齐一个偶数地址。这样,当你看到一个设置了最低有效位的地址时,你可以确定它实际上不是一个地址而是即时数据,即字符串本身。
仅使用malloc
分配2字节对齐的内存有效:您分配2个额外字节。如果malloc
返回的地址已正确对齐,则返回下一个正确对齐的地址(char_ptr + 2
)并在该地址之前直接标记该单元格,其中某个值表示原始地址已对齐( char_ptr[1] = '1'
)。另一方面,如果返回的地址未正确对齐,则返回直接跟随的字节(正确对齐; char_ptr + 1
)并在该地址之前直接标记单元格(因此,第一个; {{ 1}})。
在释放时,直接在传入的地址之前查看单元格,它包含标记,告诉您char_ptr[0] = '0'
需要哪个地址。
在代码中:
free
创建和销毁“指针或立即数据”结构非常简单:
#define IS_ALIGNED_2BYTE(value) (((uintptr_t)(value) & 0x01) == 0)
/// Allocate a memory region of the specified size at an even (2 byte
/// aligned) address.
///
/// \param[in] size Required size of the memory region.
/// \return Pointer to the memory, or NULL on failure.
inline static void * allocate_2byte_aligned(size_t size) {
#ifdef HAVE_ALIGNED_ALLOC
return aligned_alloc(2, size);
#elif defined(HAVE_POSIX_MEMALIGN)
void * ptr;
if (posix_memalign(&ptr, sizeof(void *), size) == 0) {
assert(IS_ALIGNED_2BYTE(ptr)); // Paranoia due to uncertainty
// about alignment parameter to
// posix_memalign.
return ptr;
} else {
return NULL;
}
#else
char * const memory = malloc(size + 2);
if (! memory) {
return NULL;
}
if (IS_ALIGNED_2BYTE(memory)) {
// memory is correctly aligned, but to distinguish from originally
// not aligned addresses when freeing we need to have at least one
// byte. Thus we return the next correctly aligned address and
// leave a note in the byte directly preceeding that address.
memory[1] = '1';
return &(memory[2]);
} else {
// memory is not correctly aligned. Leave a note in the first byte
// about this for freeing later and return the next (and correctly
// aligned) address.
memory[0] = '0';
return &(memory[1]);
}
#endif
}
/// Free memory previously allocated with allocate_2byte_aligned.
///
/// \param[in] ptr Pointer to the 2 byte aligned memory region.
inline static void free_2byte_aligned(void * ptr) {
assert(IS_ALIGNED_2BYTE(ptr));
#if defined(HAVE_ALIGNED_ALLOC) || defined(HAVE_POSIX_MEMALIGN)
free(ptr);
#else
char const * const memory = ptr;
void const * original_address;
if (memory[-1] == '0') {
// malloc returned an address that was not aligned when allocating
// this memory block. Thus we left one byte unused and returned
// the address of memory[1]. Now we need to undo this addition.
original_address = &(memory[-1]);
} else {
// malloc returned an address that was aligned. We left two bytes
// unused and need to undo that now.
assert(memory[-1] == '1');
original_address = &(memory[-2]);
}
free((void *) original_address);
#endif
}
上面的代码实际上来自a small library I wrote来测试/写出这个答案。如果你想,那就使用/增强它。
答案 1 :(得分:0)
如果你真的有一个长度为一个字符的“许多字符串”,那么至少有以下一项必须为真:
您的字符长度超过八位
您可以通过"interning"字符串大大受益,这样就不会创建同一字符串的多个副本。
如果你的字符串是不可变的(或者你可以安排它们不被变异),那么#2是一个非常有吸引力的选择。在#1为假并且您只有255个可能的单字符字符串的情况下,您可以通过索引到510字节的预构建表(交替的单个字符和NUL)来实习。更一般的实习策略需要哈希表或其他一些,并且还有更多的工作(但可能非常有价值)。
如果您的“字符”实际上是短字节序列但不经常重复,那么您可能希望实现一个简单的池分配方案。如果字符串中没有太多的“流失”,这将是最简单/最有效的;也就是说,你不经常分配并立即释放字符串。
一个简单的字符串池分配方案是选择一些截止值并将该大小的所有字符串按顺序分配到足够大以容纳许多短字符串的块中。大小将取决于您的精确应用,但我已经取得了一些成功,其中“短”字符串最多31个字节,块大小恰好是4096字节总是在4096字节边界上分配(使用{例如{1}}。池分配器维护一个块,它附加新分配的字符串。它还保留一个已分配的块列表,每个块都有一个活动字符串计数。 posix_memalign
如果字符串长于截止值,则推迟pool_malloc(n)
;否则,如果当前块已满,则在首次分配新块之后将新字符串放在当前活动块的末尾。 malloc(n)
检查pool_free(s)
是否足够短以适应大块。如果没有,它只调用s
,如果是,则查找活动块列表中的块并减少其活动字符串计数。 (很容易找到块,因为块都是4k对齐的事实。)这个主题有很多变化:你可以将元数据放入块本身而不是保留外部元数据;你可以简单地释放短字符串,直到整个池被释放(如果有大量的空闲,这可以节省大量的时间,而不需要额外的内存使用);你可以制作固定长度字符串的块,这样可以更容易地立即释放字符串(但需要你保留某种空闲列表结构。)
这两种策略可以组合使用;如果您能够在一次操作中释放整个实习生表/内存池,这是特别有效的,这通常是可能的。 (例如,参见Apache Portable Runtime内存分配方法。)