理解内存分配的工作原理(LLVM)

15

我正在开发一个玩具编译器(第一次),并试图理解如何分配/构建LLVM结构类型。

《Kaleidoscope》教程没有包括或提及这个问题,我也不知道在LLVM源代码/测试中找到可能的示例需要寻找什么。

因此,我编写了一个简单的C++示例,并使用clang将IR转储出来,以尝试理解它生成了什么,但说实话我并不完全明白。对我来说显然的是函数定义/声明和一些函数调用以及memset调用,所以我掌握部分内容,但还没有全部理解。(P.S我的对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(如果您感觉慷慨,还可以告诉如何稍后释放内存)。

我注释掉的位是来自我的原始示例,但作为一个完全的新手,那里的IR甚至更不具有洞察力...


有一个看起来很有前途的网站:https://mapping-high-level-constructs-to-llvm-ir.readthedocs.io/en/latest/basic-constructs/structures.html - Marco Luglio
2个回答

19
我对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而言,结构体只是一堆位,所有内存位置基本上都是相同的。如果您希望这些位成为特定的内容,则将要存储的位存储到该位置。如果您想将这些位放在堆上,则调用运行时库堆分配函数并将这些位存储到该位置。
请注意,垃圾回收实际上是一个有些不同的故事,因为存在一些关于在堆栈上查找局部变量以进行标记的棘手问题。
值得一提的是,您尝试理解Clang的LLVM IR是行不通的。我已经这样做了几年,它非常疯狂,需要很长时间才能开始掌握,更不用说充满了您不想知道的C++特定ABI细节。在他们的IRC频道中询问#llvm或在此处提出具体问题比尝试反向工程要好得多。

2
我明白了,那很有道理。感谢您的快速回复。 - zcourts
1
这纯粹是巧合。 - Puppy

9
我不建议查看Clang发出的未经优化的IR,它的输出过于冗长。使用-O1可以使其更易读-这是带有注释行的-O1版本(我还重新排列了两行代码以使其更易读):

%struct.Alloc = type { i32 }                   ; 定义Alloc类型。
define noalias %struct.Alloc* @_Z8allocPtrv() #0 { %1 = tail call noalias i8* @_Znwj(i32 4) #2 ; 调用_Znwj(4)函数,返回i8*。 %3 = bitcast i8* %1 to i32* ; 将返回值强制转换为i32*(int*)... store i32 0, i32* %3, align 4 ; ...并将其内容清零。 %2 = bitcast i8* %1 to %struct.Alloc* ; 将返回值强制转换为Alloc*... ret %struct.Alloc* %2 ; ...并返回它。 } ; 声明_Znwj函数。由于它已经在libstdc++中定义,因此无需再次定义 ; 您可以通过C ++解码器(例如http://demangler.com/)将此字符串传递来查看它。 declare noalias i8* @_Znwj(i32) #1
define i32 @main() #0 { %1 = tail call %struct.Alloc* @_Z8allocPtrv() ; 调用_Z8allocPtrv(上文已定义)。 ret i32 0 }

这是一个new调用,而不是局部分配,因此离开@_Z8allocPtrv时不会被清除。在LLVM IR中确实使用alloca指令执行局部分配,而不是new调用。

如果你想知道new的实现方式,我相信其标准实现使用malloc,由编译库的编译器将其翻译为包含系统调用的某些函数。


1
哦,太棒了,这些注释非常有用。 - zcourts

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