【问题标题】:Understanding how memory allocation works (LLVM)了解内存分配的工作原理(LLVM)
【发布时间】:2016-02-05 15:23:29
【问题描述】:

我在玩具编译器上取得了进展(第一次),并试图了解如何分配/构造 LLVM 结构类型。 Kaleidoscope 教程没有包括甚至提到这一点,我不知道我在 LLVM 源代码/测试中寻找什么以找到可能的示例。

所以我写了一个简单的 C++ 示例,用 clang 转储了 IR,试图理解它产生了什么,但老实说,我并没有完全遵循它。对我来说显而易见的是函数定义/声明和一些函数调用以及memset 调用,所以我得到了它的一部分,但对我来说还没有全部组合在一起。 (PS 我对 alloca 指令文档的解释是它创建的任何东西都会在返回时被释放,所以我不能使用它,它基本上只用于局部变量?)

我所做的是:

alloc.cpp

struct Alloc {
  int age;
};

//Alloc allocCpy() {
//  return *new Alloc();
//}

Alloc *allocPtr() {
  return new Alloc();
}

int main() {
  Alloc *ptr = allocPtr();
//  ptr->name = "Courtney";
//  Alloc cpy = allocCpy();
//  cpy.name = "Robinson";
//  std::cout << ptr->name << std::endl;
//  std::cout << cpy.name << std::endl;
  return 0;
}

然后运行clang -S -emit-llvm alloc.cpp生成alloc.ll

; ModuleID = 'alloc.cpp'
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.11.0"

%struct.Alloc = type { i32 }

; Function Attrs: ssp uwtable
define %struct.Alloc* @_Z8allocPtrv() #0 {
entry:
  %call = call noalias i8* @_Znwm(i64 4) #3
  %0 = bitcast i8* %call to %struct.Alloc*
  %1 = bitcast %struct.Alloc* %0 to i8*
  call void @llvm.memset.p0i8.i64(i8* %1, i8 0, i64 4, i32 4, i1 false)
  ret %struct.Alloc* %0
}

; Function Attrs: nobuiltin
declare noalias i8* @_Znwm(i64) #1

; Function Attrs: nounwind
declare void @llvm.memset.p0i8.i64(i8* nocapture, i8, i64, i32, i1) #2

; Function Attrs: ssp uwtable
define i32 @main() #0 {
entry:
  %retval = alloca i32, align 4
  %ptr = alloca %struct.Alloc*, align 8
  store i32 0, i32* %retval
  %call = call %struct.Alloc* @_Z8allocPtrv()
  store %struct.Alloc* %call, %struct.Alloc** %ptr, align 8
  ret i32 0
}

attributes #0 = { ssp uwtable "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="core2" "target-features"="+cx16,+sse,+sse2,+sse3,+ssse3" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { nobuiltin "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="core2" "target-features"="+cx16,+sse,+sse2,+sse3,+ssse3" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #2 = { nounwind }
attributes #3 = { builtin }

!llvm.module.flags = !{!0}
!llvm.ident = !{!1}

!0 = !{i32 1, !"PIC Level", i32 2}
!1 = !{!"clang version 3.7.0 (tags/RELEASE_370/final)"}

有人能解释一下这个 IR 中发生了什么以及它如何映射回 C++ 吗?或者忽略这个特定示例,如何/应该如何为 LLVM StructType 分配堆内存,该 LLVM StructType 超出了创建它的函数(如果您觉得很慷慨,以后如何释放内存)。

我注释掉的部分来自我原来的例子,但作为一个完全新手,IR 的洞察力更差......

【问题讨论】:

标签: c++ memory-management clang llvm llvm-ir


【解决方案1】:

我对 alloca 指令文档的解释是任何东西 从中创建的会在返回时被释放,所以我不能使用它,它是 基本上只适用于局部变量?

是的。此外,目前对 LLVM IR 的建议是,尽管 alloca 可以按您的预期工作,但优化是另一种情况。他们建议您立即在入口块中alloca所有本地人,即使您不允许用户访问他们或者他们并不总是包含有意义的数据。

堆分配是一种库功能。它不是 LLVM 或编译器的功能。当您使用new T() 时,编译器只需调用operator new 来获取内存,然后在那里构造T。没有魔法。您看到的大多数垃圾都是 C++-ABI 特定的,而不是 LLVM 的任何要求。它最终会降低为void* p = malloc(size); new(p) T(); 之类的东西。对于几乎所有类型的T,这几乎可以归结为p 中的一系列商店或调用用户定义的函数。

您可以使用您选择的运行时库中的内存分配功能。

试图了解如何分配/构造 LLVM 结构类型

LLVM 类型系统不包含构造的概念。这是源语言的概念。

就 LLVM 而言,结构只是一堆位,所有内存位置或多或少都相同。如果您希望这些位成为特定事物,则将您想要的位存储到该位置。如果要将位放在堆上,则调用运行时库堆分配函数并将位存储到该位置。

但请注意,垃圾收集是一个不同的故事,因为 w.r.t 会发生一些尴尬的事情。在堆栈上查找局部变量以进行标记。

郑重声明,您不会试图理解 Clang 的 LLVM IR。我已经这样做了好几年了,这太疯狂了,你需要很长时间才能开始掌握,更不用说你不想知道的 C++ 特定的 ABI 细节了。您将在他们的 IRC 频道中的#llvm 中获得更多询问,或者在此处询问具体问题,而不是尝试对其进行逆向工程。

【讨论】:

  • 我明白了,这是有道理的。感谢您的快速回复。
  • 纯属巧合。
【解决方案2】:

我不建议查看 Clang 发出的未优化 IR - 这太冗长了。 -O1 使其更具可读性 - 这是带有 cmets 注释行的 -O1 版本(我还重新排序了两行以使其更具可读性):

%struct.Alloc = 类型 { i32 } ;定义分配类型。 定义 noalias %struct.Alloc* @_Z8allocPtrv() #0 { %1 = 尾调用 noalias i8* @_Znwj(i32 4) #2 ;调用 _Znwj(4)。这将返回 i8*。 %3 = 比特广播 i8* %1 到 i32* ;将返回值转换为 i32* (int*)... 存储 i32 0, i32* %3, 对齐 4 ; ...并将其内容归零。 %2 = 比特广播 i8* %1 到 %struct.Alloc* ;将返回值强制转换为 Alloc*... ret %struct.Alloc* %2 ; ...并返回它。 } ;声明 _Znwj 函数。这不需要定义,因为它已经定义了 ;在 libstdc++ 中:这是“新操作员”。您可以通过将此字符串传递给 ; C++ demangler,例如 http://demangler.com/ 上的那个。 声明 noalias i8* @_Znwj(i32) #1 定义 i32 @main() #0 { %1 = 尾调用 %struct.Alloc* @_Z8allocPtrv() ;调用 _Z8allocPtrv(如上定义)。 ret i32 0 }

这是一个new调用,不是本地分配,所以离开@_Z8allocPtrv时不会被清除。本地分配确实是在 LLVM IR 中使用 alloca 指令执行的,而不是 new 调用。

如果你好奇new 是如何工作的,我相信它的标准实现使用了malloc,它由编译库的编译器翻译成一些包含系统调用的函数)。

【讨论】:

  • 哦,整洁,cmets 非常有用
猜你喜欢
  • 1970-01-01
  • 2016-02-20
  • 2012-12-11
  • 2011-02-18
  • 2018-02-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多