何时在Objective-C代码中直接使用objc_memmove_collectable?

4

最近我在头文件和文档中寻找其他内容时,发现了以下函数原型:<objc/objc-auto.h>:

void *objc_memmove_collectable(void *dst, const void *src, size_t size);

这是一组函数中的一个,在注释“写屏障。编译器使用”后面。这些函数用于通知垃圾回收器管理的内存已被修改,以便它可以扫描引用并不会意外地回收应由强引用保留但其不知道的内存。我知道适当使用__strong几乎总是会导致编译器插入正确的函数,但我也听到过苹果工程师说有极少数情况下必须直接调用这些函数。(我认为这是在文件未编译为Objective-C时发生的,例如写入GC管理的内存的C代码,但我不确定。)
因此,苹果的Objective-C Runtime Release Notes for Mac OS X v10.5文档对该函数有以下说明:(第一个标题下的最后一段)
“在大量复制内存进入垃圾收集块时,您必须使用API objc_memmove_collectable(void *dst, const void *src, size_t size)。”
该函数似乎是针对将非GC内存中的项目移动到GC内存的,电子邮件讨论档案似乎表明其目的仅是为了触发单个写入屏障以进行大块复制。(例如,1000个单独的写入带有每个写入屏障,与1个批量复制带有整个内存区域的单个写入屏障相比。)这是必须使用的一种情况,但文档没有说明何时不应该(或不需要)使用它。
例如,我有一个使用NSAllocateCollectable()NSScannedOption分配的内存块,并将其用作动态扩展的循环缓冲区。如果缓冲区变满,我会使用NSReallocateCollectable()NSScannedOption将其大小加倍。环绕的部分(在数组中的第一个插槽和缓冲区中的最后一个对象之间)将被复制/移动到数组的第二半部分的开头。然后,我使用bzero()清除从中复制数据的插槽,以避免过度根据已移动的对象进行根处理。(请参见this file中的460-467行。是的,代码可以正常运行 - 它已经完全通过单元测试,自从我一段时间前添加了__strong属性以来,我没有看到任何崩溃。)
问题:
什么时候需要使用objc_memmove_collectable()而不是memmove()memcpy()?例如,如果源和目标都是GC管理的内存,会怎样?(我的内存声明为__strong id *array;,所以我猜编译器会插入屏障。)如果不需要,使用它会帮助/阻碍GC性能吗?(例如,它是否持有任何类型的锁,或者帮助GC避免手动扫描?)这被认为是良好/差劲的风格吗?

编辑: 由于memcpymemmove完全不涉及GC,我想知道为什么我没有看到任何由于内存被收集而导致的崩溃。我目前的最佳猜测是,由于bzero也不会告诉GC任何信息,因此收集器在下一次扫描整个内存块之前不会发现清零的内存和移动的数据。如果收集器仍然将现在清零的引用视为根,并且尚未计算新的内存位置,那么这就解释了为什么值不会过早地被收集。这听起来正确吗?

2个回答

3
我相信 @bbum 会提供更加详细和明确的答案,但这是我的理解,可能不完全正确。正如头部注释所述,每当您批量写入数据(即使用 memcpy 或 memmove,而不是 =)到扫描的、GC分配的缓冲区时,您应该始终使用 objc_memmove_collectable 创建写屏障。数据的来源并不重要。memcpy 不具备智能判断是否从扫描的、GC分配的内存复制到其他扫描的、GC分配的内存。
我不确定没有写屏障 GC 是否会出现功能错误,但它肯定会表现得更好。写屏障是向收集器发出的提示,表示“嘿! 我可能在这里写了一个指针。你能为我检查一下吗? ”。没有这些,收集器就必须一直进行全面扫描。
此外,单个 objc_memmove_collectable() 比一堆隐式写屏障的 = 赋值要高效得多,即使您完全忽略实际写入内存的成本。只有一个写屏障被创建,而不是 N 个。

谢谢,这是一个好的提示,如果总是使用那个调用是安全的,似乎更可取。不过这让我想到了收集器内部的响应方式——它是否会实际保存该内存的扫描或者延迟扫描?我并不担心在那个层面上的性能调整,我更关心正确性,但我仍然很好奇... :-) - Quinn Taylor

1

我个人在这个领域没有太多经验,但我在这里找到了一个有趣的讨论链接


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接