![e4fac8c9c3ad9a8c50d8006219219019.png](https://img-blog.csdnimg.cn/img_convert/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