llvm 对 copy 属性的优化(2)

时间:2022-07-28
本文章向大家介绍llvm 对 copy 属性的优化(2),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

前言

本文将会回答两个问题:

  • 什么场景会调用 objc_copyCppObjectAtomic 函数? 在上篇文章中,我们并没有发现任何场景会调用 objc_copyCppObjectAtomic 函数。
  • copyHelper 是如何生成的? iOS 中 copy 的原理中曾经提到作者没有找到 copyHelper(dest, src); 的实现方法。

含 c++ 类的复制行为

为了解释前言中的两个问题,我们需要在 CopyMock 新增了一个属性 str,该属性的类型是 std::string

#include <string>

@interface CopyMock : NSObject

@property (readwrite) std::string str;

@end

与第一篇文章类似,我们先将代码编译为中间码。如下所示:

// 赋值函数
; Function Attrs: noinline optnone ssp uwtable
define internal void @"1-[CopyMock setStr:]"(%0*, i8*, %"class.std::__1::basic_string"*) #0 !dbg !1897 {
  %4 = alloca %0*, align 8
  %5 = alloca i8*, align 8
  store %0* %0, %0** %4, align 8
  call void @llvm.dbg.declare(metadata %0** %4, metadata !1900, metadata !DIExpression()), !dbg !1901
  store i8* %1, i8** %5, align 8
  call void @llvm.dbg.declare(metadata i8** %5, metadata !1902, metadata !DIExpression()), !dbg !1901
  call void @llvm.dbg.declare(metadata %"class.std::__1::basic_string"* %2, metadata !1903, metadata !DIExpression()), !dbg !1901
  %6 = load %0*, %0** %4, align 8, !dbg !1904
  %7 = bitcast %0* %6 to i8*, !dbg !1904
  %8 = getelementptr inbounds i8, i8* %7, i64 32, !dbg !1904
  %9 = bitcast i8* %8 to %"class.std::__1::basic_string"*, !dbg !1904
  %10 = bitcast %"class.std::__1::basic_string"* %9 to i8*, !dbg !1904
  %11 = bitcast %"class.std::__1::basic_string"* %2 to i8*, !dbg !1904
  call void @objc_copyCppObjectAtomic(i8* %10, i8* %11, i8* bitcast (void (%"class.std::__1::basic_string"*, %"class.std::__1::basic_string"*)* @__assign_helper_atomic_property_ to i8*)), !dbg !1904
  ret void, !dbg !1904
}

编译器会先帮开发者增加一个普通的赋值方法 -[CopyMock setStr:]

该方法最后会调用 objc_copyCppObjectAtomic函数, 三个参数分别是:

  • 属性str的存储地址,CopyMock 实例的偏移 32 位 `%8 = getelementptr inbounds i8, i8* %7, i64 32, !dbg !1904`
  • 被复制 str 的地址 `%11 = bitcast %"class.std::__1::basic_string"* %2 to i8*, !dbg !1904`
  • 由编译器生成的辅助复制函数 `__assign_helper_atomic_property_`

__assign_helper_atomic_property_

在分析 __assign_helper_atomic_property_ 的内部逻辑前,我们需要再看一遍 objc_copyCppObjectAtomic 函数的声明: void objc_copyCppObjectAtomic(void *dest, const void *src, void (*copyHelper) (void *dest, const void *source))

通过原型我们可以发现 copyHelperobjc_copyCppObjectAtomic 的第三个参数。

实际上,void (*copyHelper) (void *dest, const void *source)) 是一个函数指针,它的实现是通过调用方传参决定。在本例中,它会指向 __assign_helper_atomic_property_

在阅读 __assign_helper_atomic_property_ 的代码前,需要先准备几个小知识:

  • c++ 的函数名会被 name mangle,所以 _ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEaSERKS5_ 实际上代表字符串复制函数 std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::operator=(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&)
  • 中间码具有 ssp 特征,阅读比较繁琐,编译后的汇编会被高度优化,下一章会讲到编译器的优化
  • c++ 中的std::stringclass.std::__1::basic_string 是等价的
  • alloca 可以在栈中开辟空间,具有效率高,不需要主动释放等特性

小知识准备结束,下面开始对 __assign_helper_atomic_property_ 的实现内容进行分析:

// 辅助赋值函数
; Function Attrs: noinline ssp uwtable
define internal void @__assign_helper_atomic_property_(%"class.std::__1::basic_string"*, %"class.std::__1::basic_string"*) #5 !dbg !1891 {
  // 在栈中开辟空间
  %3 = alloca %"class.std::__1::basic_string"*, align 8

  // 在栈中开辟空间,备用
  %4 = alloca %"class.std::__1::basic_string"*, align 8

  // 存储参数 %0 到 %3
  store %"class.std::__1::basic_string"* %0, %"class.std::__1::basic_string"** %3, align 8
  call void @llvm.dbg.declare(metadata %"class.std::__1::basic_string"** %3, metadata !1892, metadata !DIExpression()), !dbg !1893

  // 存储参数 %1 到 %4
  store %"class.std::__1::basic_string"* %1, %"class.std::__1::basic_string"** %4, align 8
  call void @llvm.dbg.declare(metadata %"class.std::__1::basic_string"** %4, metadata !1894, metadata !DIExpression()), !dbg !1893

  // 取出参数 %4 到 %5
  %5 = load %"class.std::__1::basic_string"*, %"class.std::__1::basic_string"** %4, align 8, !dbg !1893
  // 取出参数 %3 到 %6
  %6 = load %"class.std::__1::basic_string"*, %"class.std::__1::basic_string"** %3, align 8, !dbg !1893
  // 调用字符串复制方法 std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::operator=(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&)
  %7 = call dereferenceable(24) %"class.std::__1::basic_string"* @_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEaSERKS5_(%"class.std::__1::basic_string"* %6, %"class.std::__1::basic_string"* dereferenceable(24) %5), !dbg !1893
  ret void, !dbg !1895
}

这个函数的逻辑很简单,相当于对 字符串复制函数 std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::operator=(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&) 进行了一次间接调用。

总结

本文通过将代码改造为 cpp 代码,可以得到以下两个信息:

  • copyHelper 通常是由编译器生成的辅助函数
  • 具有 cpp 相关类复制的场景才会触发对 objc_copyCppObjectAtomic 函数的调用