llvm 实现一门语言_llvm-hs使用之初体验

e4fac8c9c3ad9a8c50d8006219219019.png

妈妈再也不用担心我不会汇编了,有了llvm-hs,可以很方便的使用简单的表达式语言来生成复杂的汇编代码了。不管是x86,arm,还是avx,neon,还是DSP,都可以生成,毫无压力,而且生成的是经过优化后的汇编代码。这些生成的汇编代码可以通过C函数来调用,这就是llvm-hs的使用初体验。

llvm是非常优秀的编译器基础架构,llvm-hs是其Haskell的一个binding。之前经常听人提到llvm-hs好用,趁着五一假期有些时间,于是下载并安装了llvm-hs,写了简单的例子,感觉还是挺好用的。

在macOS上安装llvm-hs还是有一些小折腾的,不像在ubuntu 18.04上,使用标准的apt-get install和cabal install安装方式就可以。在macOS上首先需要使用如下命令来安装llvm-9.0。

brew install llvm-hs/llvm/llvm-9

我的ghc安装的版本是8.10.1,cabal版本是3.2.0,直接使用如下命令安装llvm-hs会出现一堆C++11扩展的错误。

cabal install llvm-hs

这些错误的原因是-std=c++11这个参数没有传递给C++编译器,虽然在cabal文件中指定了cc-options=-std=c++11,但是没有起作用。在google了一通无果后,我想到以前在编译ghc的交叉编译器时曾经改过ghc中调用C编译器的设置,于是顺着这个线索找到了ghc的lib目录下的settings文件。将这个文件的C++编译参数设置这一行改为下面的样子,问题解决了。

,("C++ compiler flags", "-std=c++11")

现在环境搭建好了,可以开始我们的llvm-hs的探索之旅了。为简单起见,我们用简单的数学计算的表达式来演示llvm-hs的使用。我们用haskell来写一个简单的表达式的解析程序,这个解析程序会将我们写的表达式解析后生成llvm的汇编,然后通过clang这个编译器驱动来生成x86、neon、高通hexagon DSP的汇编代码。

首先,我们先定义表达式的支持的操作语义,这里我们使用F-alg的形式来定义,具体如下:

data ExprF a e
  = -- | a 'Double' literal
    LitF Double
  | -- | @a+b@
    AddF e e
  | -- | @a-b@
    SubF e e
  | -- | @a*b@
    MulF e e
  | -- | @a/b@
    DivF e e
  | -- | @-a@
    NegF e
  | -- | @'exp' a@
    Exp e
  | -- | @'log' a@
    Log e
  | -- | @'sqrt' a@
    Sqrt e
  | -- | @'sin' a@
    Sin e
  | -- | @'cos' a@
    Cos e
  | -- | @'x'@
    VarF
  deriving (Functor, Foldable, Traversable)

type Expr a = Fix (ExprF a)

Expr a是ExprF a e 的不动点,这样我们在后面就可以很方便的使用cata 函数加上不同的alg 函数来完成对数学表达式做不同的解析,完成比如打印表达式、计算结果、生成llvm汇编代码等不同的功能。

为了方便构造表达式,另外定义了如下这些辅助函数:

x :: Expr a
x = Fix VarF

litf :: Double -> Expr Double
litf d = Fix (LitF d)

addf :: Expr a -> Expr a -> Expr a
addf a b = Fix (AddF a b)

subf :: Expr a -> Expr a -> Expr a
subf a b = Fix (SubF a b)

mulf :: Expr a -> Expr a -> Expr a
mulf a b = Fix (MulF a b)

divf :: Expr a -> Expr a -> Expr a
divf a b = Fix (DivF a b)

negf :: Expr a -> Expr a
negf a = Fix (NegF a)

再将Expr Double实现为Num、 Fractional、Floating的类型类实例。

instance Num (Expr Double) where
  fromInteger = litf . fromInteger
  (+) = addf
  (-) = subf
  (*) = mulf
  negate = negf
  abs = notImplemented "Expr.abs"
  signum = notImplemented "Expr.signum"

instance Fractional (Expr Double) where
  (/) = divf
  recip = divf 1
  fromRational = litf . fromRational

instance Floating (Expr Double) where
  pi = litf pi
  exp = Fix . Exp
  log = Fix . Log
  sqrt = Fix . Sqrt
  sin = Fix . Sin
  cos = Fix . Cos
  asin = notImplemented "Expr.asin"
  acos = notImplemented "Expr.acos"
  atan = notImplemented "Expr.atan"
  sinh = notImplemented "Expr.sinh"
  cosh = notImplemented "Expr.cosh"
  asinh = notImplemented "Expr.asinh"
  acosh = notImplemented "Expr.acosh"
  atanh = notImplemented "Expr.atanh"

notImplemented :: String -> a
notImplemented = error . (++ " is not implemented")

这样我们就可以象写一般的数学计算表达式一样来构造我们的表达式Expr Double了。

f :: Floating a => a -> a
f t = sin (pi * t / 2) * (1 + sqrt t) ^ 2

我们通过分别调用函数printExpr和printCodegen来打印出这个表达式,生成llvm的汇编代码。

-- 打印出表达式
  printExpr ((f x) :: Expr Double)
-- 生成llvm的汇编代码
  printCodegen ((f x) :: Expr Double)

我们如何使用同样的表达式来完成这两个不同的功能的呢,秘诀就是Expr Double是Expr Double e的不动点,我们将不同的alg函数作为参数传给cata函数就可以达到这个目的了。

我们先看打印表达式的实现,首先实现一个打印的alg函数,如下所示。

ppExpAlg :: ExprF a (Expr a, String) -> String
ppExpAlg (LitF d) = show d
ppExpAlg (AddF (_, a) (_, b)) = a ++ " + " ++ b
ppExpAlg (SubF (_, a) (e2, b)) =
  a ++ " - " ++ paren (isAdd e2 || isSub e2) b
ppExpAlg (MulF (e1, a) (e2, b)) =
  paren (isAdd e1 || isSub e1) a ++ " * " ++ paren (isAdd e2 || isSub e2) b
ppExpAlg (DivF (e1, a) (e2, b)) =
  paren (isAdd e1 || isSub e1) a ++ " / " ++ paren (isComplex e2) b
ppExpAlg (NegF (_, a)) = function "negate" a
ppExpAlg (Exp (_, a)) = function "exp" a
ppExpAlg (Log (_, a)) = function "log" a
ppExpAlg (Sqrt (_, a)) = function "sqrt" a
ppExpAlg (Sin (_, a)) = function "sin" a
ppExpAlg (Cos (_, a)) = function "cos" a
ppExpAlg VarF = "x"

然后将这个alg函数传递给cata函数,如下所示:

-- | Pretty print an 'Expr'
pp :: Expr a -> String
pp e = funprefix ++ para ppExpAlg e
  where
    funprefix = "x -> "

printExpr :: MonadIO m => Expr a -> m ()
printExpr expr = liftIO $ do
  putStrLn "*** Expression ***n"
  putStrLn (pp expr)

就可以打印出我们上面构造出的表达式了。

好了,我们现在来看llvm的汇编代码是如何生成的,同样的,先实现下面的生成llvm汇编代码的alg函数。

    alg arg _ (LitF d) =
      return (LLVM.ConstantOperand $ LLVM.Float $ LLVM.Double d)
    alg arg _ VarF = return arg
    alg arg _ (AddF a b) = LLVMIR.fadd a b `LLVMIR.named` "x"
    alg arg _ (SubF a b) = LLVMIR.fsub a b `LLVMIR.named` "x"
    alg arg _ (MulF a b) = LLVMIR.fmul a b `LLVMIR.named` "x"
    alg arg _ (DivF a b) = LLVMIR.fdiv a b `LLVMIR.named` "x"
    alg arg ps (NegF a) = do
      z <- alg arg ps (LitF 0)
      LLVMIR.fsub z a `LLVMIR.named` "x"
    alg arg ps (Exp a) = callDblfun (ps Map.! "exp") a `LLVMIR.named` "x"
    alg arg ps (Log a) = callDblfun (ps Map.! "log") a `LLVMIR.named` "x"
    alg arg ps (Sqrt a) = callDblfun (ps Map.! "sqrt") a `LLVMIR.named` "x"
    alg arg ps (Sin a) = callDblfun (ps Map.! "sin") a `LLVMIR.named` "x"
    alg arg ps (Cos a) = callDblfun (ps Map.! "cos") a `LLVMIR.named` "x"

将这个函数传递给cataM函数,这里使用cataM是因为在Monad的环境中调用的。

codegen :: Expr a -> LLVM.Module
codegen fexpr = LLVMIR.buildModule "arith.ll" $ do
  prims <- declarePrimitives fexpr
  _ <- LLVMIR.function "f" [(LLVM.double, xparam)] LLVM.double $ [arg] -> do
    res <- cataM (alg arg prims) fexpr
    LLVMIR.ret res
  return ()

把生成的llvm模块使用如下的pretty print函数打印出来,就得到了llvm的汇编代码了。

codegenText :: Expr a -> Text
codegenText = LLVMPretty.ppllvm . codegen

printCodegen :: Expr a -> IO ()
printCodegen = Text.putStrLn . codegenText

上面这个表达式只支持双精度浮点数Double,还不能支持Int32,SIMD指令中的Int32x4这些数据类型。下面我们就来加上Int32和SIMD的Int32x4的支持吧,为节省篇幅,这里只介绍如何加上SIMD的Int32x4的支持的过程。

首先,我们扩展原来的表达式支持的操作语义,使其可以支持SIMD的Int32x4数据类型。下面的定义直接附加在上面的Expr a e的操作语义定义后面即可。

  | -- | a 'Int32x4' literal
    LitVI Int32x4
  | -- | @a+b@
    AddVI e e
  | -- | @a-b@
    SubVI e e
  | -- | @a*b@
    MulVI e e
  | -- | @a/b@
    DivVI e e
  | -- | @-a@
    NegVI e
  | -- | @'vx'@
    VarVI

同样的,增加一些表达式构造的辅助函数。

vx :: Expr a
vx = Fix VarVI

litvi :: Int32x4 -> Expr Int32x4
litvi i = Fix (LitVI i)

cvec :: [Int32] -> Expr Int32x4
cvec = litvi . fromList

addvi :: Expr a -> Expr a -> Expr a
addvi a b = Fix (AddVI a b)

subvi :: Expr a -> Expr a -> Expr a
subvi a b = Fix (SubVI a b)

mulvi :: Expr a -> Expr a -> Expr a
mulvi a b = Fix (MulVI a b)

divvi :: Expr a -> Expr a -> Expr a
divvi a b = Fix (DivVI a b)

negvi :: Expr a -> Expr a
negvi a = Fix (NegVI a)

再将Expr Int32x4实现为Num、 Fractional的类型类实例。

type Int16x4 = Vec 4 Int16
type Int32x4 = Vec 4 Int32

instance Num (Expr Int32x4) where
  fromInteger = litvi . replicateVec . fromInteger
  (+) = addvi
  (-) = subvi
  (*) = mulvi
  negate = negvi
  abs = notImplemented "Expr.abs"
  signum = notImplemented "Expr.signum"

instance Fractional (Expr Int32x4) where
  (/) = divvi
  recip = notImplemented "Expr.recip"
  fromRational = notImplemented "Expr.intRatioinal"

于是,我们也可以象写一般的数学计算表达式一样来构造我们的表达式Expr Int32x4了。

fv :: Expr Int32x4 -> Expr Int32x4
fv t = 3 * (-t) + (4 / t) * t ^ 3

分别调用函数printExpr和printCodegen来打印出这个表达式,生成llvm的汇编代码。

  printExpr ((fv vx) :: Expr Int32x4)
  printCodegen ((fv vx) :: Expr Int32x4)

我们在alg函数中增加Expr Int32x4的支持,就可以打印出Expr Int32x4的表达式了。

ppExpAlg (LitVI i32) = show i32
ppExpAlg (AddVI (_, a) (_, b)) = a ++ " + " ++ b
ppExpAlg (SubVI (_, a) (e2, b)) =
  a ++ " - " ++ paren (isAdd e2 || isSub e2) b
ppExpAlg (MulVI (e1, a) (e2, b)) =
  paren (isAdd e1 || isSub e1) a ++ " * " ++ paren (isAdd e2 || isSub e2) b
ppExpAlg (DivVI (e1, a) (e2, b)) =
  paren (isAdd e1 || isSub e1) a ++ " / " ++ paren (isComplex e2) b
ppExpAlg (NegVI (_, a)) = paren True ("-" ++ a)

再在生成代码的alg函数中增加Expr Int32x4的支持,就可以生成llvm的汇编了。

    alg arg _ (LitVI i32) =
      return (LLVM.ConstantOperand $ LLVM.Vector (fmap (LLVM.Int 32 . toInteger) $ V.toList i32))
    alg arg _ VarVI = return arg
    alg arg _ (AddVI a b) = LLVMIR.add a b `LLVMIR.named` "x"
    alg arg _ (SubVI a b) = LLVMIR.sub a b `LLVMIR.named` "x"
    alg arg _ (MulVI a b) = LLVMIR.mul a b `LLVMIR.named` "x"
    alg arg _ (DivVI a b) = LLVMIR.sdiv a b `LLVMIR.named` "x"
    alg arg ps (NegVI a) = do
      z <- alg arg ps (LitVI (replicateVec 0))
      LLVMIR.sub z a `LLVMIR.named` "x"

上面的Expr Int32x4表达式fv 生成的llvm汇编如下所示:

; ModuleID = 'arith.ll'
source_filename = "<string>"

define external ccc  <4 x i32> @fv(<4 x i32>  %x_0)    {
  %x_1 = sub   <4 x i32> < i32 0, i32 0, i32 0, i32 0 >, %x_0
  %x_2 = mul   <4 x i32> < i32 3, i32 3, i32 3, i32 3 >, %x_1
  %x_3 = sdiv  <4 x i32> < i32 4, i32 4, i32 4, i32 4 >, %x_0
  %x_4 = mul   <4 x i32> %x_0, %x_0
  %x_5 = mul   <4 x i32> %x_4, %x_0
  %x_6 = mul   <4 x i32> %x_3, %x_5
  %x_7 = add   <4 x i32> %x_2, %x_6
  ret <4 x i32> %x_7
}

可以看到,上面的llvm汇编是没有优化的,不过不要担心,我们生成目标平台相关的汇编代码的过程中会对上面的llvm汇编代码做优化。

我们接下来就可以调用clang这个编译器驱动来完成优化和生成最终的目标平台相关的汇编代码了。先用命令 clang -O2 -S -o simd-example-opt.s simd-example.ll 来生成当前host平台的汇编代码,我使用的是苹果的MBP15笔记本,因此生成的是avx的simd指令。

	.section	__TEXT,__text,regular,pure_instructions
	.macosx_version_min 10, 15
	.section	__TEXT,__literal16,16byte_literals
	.p2align	4               ## -- Begin function fv
LCPI0_0:
	.long	4294967293              ## 0xfffffffd
	.long	4294967293              ## 0xfffffffd
	.long	4294967293              ## 0xfffffffd
	.long	4294967293              ## 0xfffffffd
	.section	__TEXT,__text,regular,pure_instructions
	.globl	_fv
	.p2align	4, 0x90
_fv:                                    ## @fv
## %bb.0:
	pextrd	$1, %xmm0, %ecx
	movl	$4, %eax
	xorl	%edx, %edx
	idivl	%ecx
	movl	%eax, %ecx
	movd	%xmm0, %esi
	movl	$4, %eax
	xorl	%edx, %edx
	idivl	%esi
	movd	%eax, %xmm2
	pinsrd	$1, %ecx, %xmm2
	pextrd	$2, %xmm0, %ecx
	movl	$4, %eax
	xorl	%edx, %edx
	idivl	%ecx
	pinsrd	$2, %eax, %xmm2
	pextrd	$3, %xmm0, %ecx
	movl	$4, %eax
	xorl	%edx, %edx
	idivl	%ecx
	pinsrd	$3, %eax, %xmm2
	movdqa	%xmm0, %xmm1
	pmulld	%xmm0, %xmm1
	pmulld	%xmm2, %xmm1
	paddd	LCPI0_0(%rip), %xmm1
	pmulld	%xmm0, %xmm1
	movdqa	%xmm1, %xmm0
	retq
                                        ## -- End function

.subsections_via_symbols

再用命令 clang -target aarch64 -O2 -S -o simd-example-opt-arm64.s simd-example.ll 来生成aarch64目标平台的汇编代码,这里生成的是neon的simd指令。看起来比avx的simd指令少多了。

	.text
	.file	"<string>"
	.globl	f                       // -- Begin function f
	.p2align	2
	.type	f,@function
f:                                      // @f
// %bb.0:
	mov	w9, #4
	fmov	w10, s0
	mov	w8, v0.s[1]
	sdiv	w10, w9, w10
	mov	w11, v0.s[2]
	sdiv	w8, w9, w8
	fmov	s1, w10
	mov	w12, v0.s[3]
	sdiv	w11, w9, w11
	mov	v1.s[1], w8
	sdiv	w9, w9, w12
	mov	v1.s[2], w11
	mul	v2.4s, v0.4s, v0.4s
	mov	v1.s[3], w9
	mvni	v3.4s, #2
	mla	v3.4s, v2.4s, v1.4s
	mul	v0.4s, v3.4s, v0.4s
	ret
.Lfunc_end0:
	.size	f, .Lfunc_end0-f
                                        // -- End function

	.section	".note.GNU-stack","",@progbits
	.addrsig

最后用命令 clang -target hexagon -O2 -S -o simd-example-opt-hexagon.s simd-example.ll 来生成高通的hexagon DSP的汇编代码(这个汇编我就看不懂了:-))。

	.text
	.file	"<string>"
	.globl	fv                      // -- Begin function fv
	.p2align	4
	.type	fv,@function
fv:                                     // @fv
// %bb.0:
	{
		r17:16 = combine(r3,r2)
		memd(r29+#-16) = r17:16
		allocframe(#40)
	}                               // 8-byte Folded Spill
	{
		r20 = r0
		r1:0 = combine(r16,#4)
		memd(r29+#16) = r21:20
		memd(r29+#24) = r19:18
	}                               // 8-byte Folded Spill
	{
		call __hexagon_divsi3
		r19:18 = combine(r5,r4)
		memd(r29+#8) = r23:22
		memd(r29+#0) = r25:24
	}                               // 8-byte Folded Spill
	{
		call __hexagon_divsi3
		r22 = r0
		r1:0 = combine(r17,#4)
	}
	{
		call __hexagon_divsi3
		r23 = r0
		r1:0 = combine(r18,#4)
	}
	{
		call __hexagon_divsi3
		r24 = r0
		r1:0 = combine(r19,#4)
	}
	{
		r5 = mpyi(r17,r17)
		r4 = mpyi(r16,r16)
		r3:2 = CONST64(#-8589934595)
		r25 = r0
	}
	{
		r7:6 = combine(r3,r2)
	}
	{
		r3 += mpyi(r5,r23)
		r2 += mpyi(r4,r22)
	}
	{
		r5 = mpyi(r19,r19)
		r4 = mpyi(r18,r18)
	}
	{
		r3 = mpyi(r3,r17)
		r2 = mpyi(r2,r16)
	}
	{
		r7 += mpyi(r5,r25)
		r6 += mpyi(r4,r24)
		memd(r20+#0) = r3:2
	}
	{
		r1 = mpyi(r7,r19)
		r0 = mpyi(r6,r18)
	}
	{
		memd(r20+#8) = r1:0
	}
	{
		r17:16 = memd(r29+#32)
		r19:18 = memd(r29+#24)
	}                               // 8-byte Folded Reload
	{
		r21:20 = memd(r29+#16)
		r23:22 = memd(r29+#8)
	}                               // 8-byte Folded Reload
	{
		r25:24 = memd(r29+#0)
		r31:30 = dealloc_return(r30):raw
	}                               // 8-byte Folded Reload
.Lfunc_end0:
	.size	fv, .Lfunc_end0-fv
                                        // -- End function

	.section	".note.GNU-stack","",@progbits
	.addrsig

由此可见,使用llvm-hs能够很方便的将高层次的数学计算的表达式转换为高效率的目标平台相关的汇编代码,特别是对simd指令的支持。我们知道,不管是avx还是neon的simd指令,特别是DSP指令,写起来都是很麻烦的,就算是使用C语言的intrinsics的方式来写,也只是比直接写汇编好一点,没有多大改变,要操心的东西太多了。寄存器的使用,内存的对齐,指令的选择和记忆,汇编一样的思维来组织程序。这些东西耗费了我们太多的脑力,如果可以从这里解放出来,是有非常大的收益的。这里的实现是这方面的一个简单的探索。

参考链接:

llvm-hs/llvm-hs-examples

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值