原始答案
{
void *mem = malloc(1024+16);
void *ptr = ((char *)mem+16) & ~ 0x0F;
memset_16aligned(ptr, 0, 1024);
free(mem);
}
固定答案
{
void *mem = malloc(1024+15);
void *ptr = ((uintptr_t)mem+15) & ~ (uintptr_t)0x0F;
memset_16aligned(ptr, 0, 1024);
free(mem);
}
按要求解释
第一步是分配足够的备用空间,以防万一。由于内存必须是 16 字节对齐的(意味着前导字节地址需要是 16 的倍数),因此添加 16 个额外字节可以保证我们有足够的空间。在前 16 个字节的某处,有一个 16 字节对齐的指针。(请注意,它应该返回一个对于任何malloc()
目的都充分对齐的指针。但是,'any' 的含义主要用于基本类型 - 、、、和指向对象的指针和指向函数的指针。当你做更专业的事情,比如玩图形系统,他们可能需要比系统的其他部分更严格的对齐——因此像这样的问题和答案。)long
double
long double
long long
下一步是将void指针转换为char指针;尽管有 GCC,但您不应该对 void 指针进行指针运算(并且 GCC 有警告选项可以在您滥用它时告诉您)。然后将 16 添加到开始指针。假设malloc()
返回了一个不可能对齐的指针:0x800001。添加 16 得到 0x800011。现在我想向下舍入到 16 字节边界——所以我想将最后 4 位重置为 0。0x0F 将最后 4 位设置为 1;因此,~0x0F
除了最后四位之外,所有位都设置为 1。加上 0x800011 得到 0x800010。您可以迭代其他偏移量并查看相同的算术是否有效。
最后一步,free()
,很简单:你总是,而且只有,返回到free()
一个值malloc()
,calloc()
或者realloc()
返回给你——其他任何事情都是一场灾难。您正确地提供mem
了保持该值-谢谢。免费发布它。
最后,如果您了解系统malloc
包的内部结构,您可能会猜到它可能会返回 16 字节对齐的数据(或者它可能是 8 字节对齐的)。如果它是 16 字节对齐的,那么您就不需要使用这些值。然而,这是狡猾且不可移植的——其他malloc
包有不同的最小对齐,因此当它做不同的事情时假设一件事会导致核心转储。在广泛的范围内,该解决方案是可移植的。
其他人提到posix_memalign()
了另一种获得对齐内存的方法;这并非在任何地方都可用,但通常可以以此为基础来实现。请注意,对齐是 2 的幂很方便;其他路线更混乱。
还有一条评论——这段代码不检查分配是否成功。
修正案
Windows Programmer指出您不能对指针执行位掩码操作,而且事实上,GCC(经过 3.4.6 和 4.3.1 测试)确实会这样抱怨。因此,下面是基本代码的修改版本——转换为主程序。正如已经指出的那样,我还冒昧地只添加了 15 而不是 16。我一直在使用uintptr_t
C99,因为它已经存在了足够长的时间,可以在大多数平台上访问。如果不是PRIXPTR
在语句中使用,那么代替使用printf()
就足够了。[此代码包含CR指出的修复,它重申了Bill K几年前首次提出的观点,直到现在我都设法忽略了这一点。]#include <stdint.h>
#include <inttypes.h>
#include <assert.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static void memset_16aligned(void *space, char byte, size_t nbytes)
{
assert((nbytes & 0x0F) == 0);
assert(((uintptr_t)space & 0x0F) == 0);
memset(space, byte, nbytes); // Not a custom implementation of memset()
}
int main(void)
{
void *mem = malloc(1024+15);
void *ptr = (void *)(((uintptr_t)mem+15) & ~ (uintptr_t)0x0F);
printf("0x%08" PRIXPTR ", 0x%08" PRIXPTR "\n", (uintptr_t)mem, (uintptr_t)ptr);
memset_16aligned(ptr, 0, 1024);
free(mem);
return(0);
}
这是一个稍微更通用的版本,它适用于 2 的幂的大小:
#include <assert.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static void memset_16aligned(void *space, char byte, size_t nbytes)
{
assert((nbytes & 0x0F) == 0);
assert(((uintptr_t)space & 0x0F) == 0);
memset(space, byte, nbytes); // Not a custom implementation of memset()
}
static void test_mask(size_t align)
{
uintptr_t mask = ~(uintptr_t)(align - 1);
void *mem = malloc(1024+align-1);
void *ptr = (void *)(((uintptr_t)mem+align-1) & mask);
assert((align & (align - 1)) == 0);
printf("0x%08" PRIXPTR ", 0x%08" PRIXPTR "\n", (uintptr_t)mem, (uintptr_t)ptr);
memset_16aligned(ptr, 0, 1024);
free(mem);
}
int main(void)
{
test_mask(16);
test_mask(32);
test_mask(64);
test_mask(128);
return(0);
}
为了转换test_mask()
为通用分配函数,分配器的单个返回值必须对释放地址进行编码,正如一些人在他们的回答中指出的那样。
面试官的问题
Uri评论说:也许我今天早上有 [a] 阅读理解问题,但如果面试问题特别说:“你将如何分配 1024 字节的内存”,而你显然分配的不止这些。这不会是面试官自动失败吗?
我的回复不适合 300 个字符的评论...
这取决于,我想。我认为大多数人(包括我)都认为这个问题的意思是“你将如何分配一个可以存储 1024 字节数据的空间,并且基地址是 16 字节的倍数”。如果面试官的意思是你如何分配 1024 字节(仅)并使其 16 字节对齐,那么选项就更有限了。
- 显然,一种可能性是分配 1024 个字节,然后对该地址进行“对齐处理”;这种方法的问题是实际可用空间没有正确确定(可用空间在 1008 到 1024 字节之间,但没有可用于指定大小的机制),这使得它不太有用。
- 另一种可能性是您应该编写一个完整的内存分配器并确保您返回的 1024 字节块是适当对齐的。如果是这种情况,您最终可能会执行与建议的解决方案非常相似的操作,但是您将其隐藏在分配器中。
但是,如果面试官期望这些回答中的任何一个,我希望他们认识到这个解决方案回答了一个密切相关的问题,然后重新构建他们的问题以将对话指向正确的方向。(此外,如果面试官真的很草率,那我就不想要这份工作;如果对一个不够精确的要求的答案在没有纠正的情况下被炮轰,那么面试官就不是可以安全工作的人。)
世界继续前进
问题的标题最近发生了变化。难倒我的是解决 C 面试问题中的内存对齐问题。修改后的标题(如何仅使用标准库分配对齐的内存?)需要稍微修改的答案——这个附录提供了它。
C11 (ISO/IEC 9899:2011) 新增功能aligned_alloc()
:
7.22.3.1aligned_alloc
功能
概要
#include <stdlib.h>
void *aligned_alloc(size_t alignment, size_t size);
说明
该aligned_alloc
函数为对象分配空间,该对象的对齐方式由 指定alignment
,其大小由 指定size
,其值为不确定。的值alignment
应该是实现支持的有效对齐方式,并且的值size
应该是 的整数倍alignment
。
返回
该aligned_alloc
函数返回一个空指针或一个指向已分配空间的指针。
POSIX 定义posix_memalign()
:
#include <stdlib.h>
int posix_memalign(void **memptr, size_t alignment, size_t size);
描述
该posix_memalign()
函数应分配size
在 指定的边界上对齐的字节alignment
,并应返回指向在 中分配的内存的指针memptr
。的值alignment
应是 的两倍的幂sizeof(void *)
。
成功完成后, 指向的值memptr
应为 的倍数alignment
。
如果请求的空间大小为 0,则行为是实现定义的;返回的值memptr
应为空指针或唯一指针。
该free()
函数应释放先前已分配的内存posix_memalign()
。
返回值
成功完成后,posix_memalign()
应返回零;否则,应返回错误号以指示错误。
现在可以使用其中一个或两个来回答这个问题,但是当最初回答这个问题时,只有 POSIX 函数是一个选项。
在幕后,新的对齐内存功能与问题中概述的工作大致相同,除了它们能够更轻松地强制对齐,并在内部跟踪对齐内存的开始,这样代码就不会必须特别处理——它只是释放所使用的分配函数返回的内存。