有一次我问为什么简单的“times 2”程序的Haskell二进制文件这么大(例如C++)?,但这被欺骗了用GHC编译成巨大的二进制文件的小Haskell程序。
我决定问一个类似的问题,但重点是代码的实际用途,因此是它的汇编形式,而不是二进制文件。 我还没有找到我的问题的现有答案。
所以不,那个不能回答我的问题。
这是 C++(实际上是 C)的哑程序,它假设给出了 1 个表示数字的命令行参数,将其转换为
int
,然后 return
将其乘以 2 作为可执行文件的退出代码(是的) ,我不能超过255)
int main(int argc, char * argv[]) {
int r{0};
int c{0};
while (argv[1][c] != '\0') {
r *= 10;
r += argv[1][c++] - '0';
}
return r*2;
}
main: # @main
mov rcx, qword ptr [rsi + 8]
movzx edx, byte ptr [rcx]
test dl, dl
je .LBB0_1
inc rcx
xor eax, eax
.LBB0_3: # =>This Inner Loop Header: Depth=1
movsx edx, dl
lea eax, [rax + 4*rax]
lea eax, [rdx + 2*rax]
add eax, -48
movzx edx, byte ptr [rcx]
inc rcx
test dl, dl
jne .LBB0_3
add eax, eax
ret
.LBB0_1:
xor eax, eax
ret
这是一个“类似的”Haskell 程序,据我所知,它完成了完全相同的任务,但它仍然生成了一个巨大的程序集:
import Data.List (foldl')
import System.Environment (getArgs)
import System.Exit (exitWith, ExitCode (ExitFailure))
main :: IO Int
main = do
[str] <- getArgs
exitWith $ ExitFailure $ (*) 2 $ foldl' step 0 str
where
step i c = 10 * i + fromEnum c - fromEnum '0'
是的,它以不同的方式做到这一点,特别是因为我无法真正用 Haskell 吐出程序代码,但我的观点是,当我编译上面的代码时,编译器应该知道那是
main
并且为计算退出值而创建的任何中间结果都不需要在其他地方使用。为什么不能证明所有这些代码所做的事情都可以通过几行汇编来完成(正如上面的 C/C++ 程序所演示的那样?)
或者,如果情况并非如此,那么为什么装配体如此之大?我只是忘记了适当的编译器标志吗?或者什么?
难道只是开发GHC的心血没有投入到优化这么小的代码上吗? (在这种情况下,我会想知道为什么,考虑到组件化意味着将一个大程序分成越来越小的程序:如果我不优化它的小部分,我怎么能有一个最优的程序?)或者是优化这样一个小程序根本不可能,因为我仍然不明白的根本原因?
我想到了另一个想法:也许我编写代码的方式不太容易被优化?也许如果我以另一种形式编写它,编译器可以做得更好?
关于优化是一个难题,是的,我相信,但我对此一无所知。但我认为给编译器的保证越多,它就越能优化。在这方面我认为 Haskell 更好。
当然,我可以花一些时间来编写 Haskell 代码,它表达的过程与我在 C 中表达的过程完全相同。但是考虑到我目前拥有的程序集的大小......它真的会产生很大的差异吗?
如果您认为是这样,请告诉我,我会删除这个问题并花时间尝试一下。
有些评论提到了类似的事情
但是为什么呢?我的意思是,
作为比较,当我读到(并验证)Clang 计算出这段 C 代码时,我感到很惊讶
int foo(int a) {
int count = 0;
while (a) {
++count;
a &= a - 1;
}
return count;
}
归结为设定的位数,因此它将其编译为
foo(int): # @foo(int)
popcntl %edi, %eax
retq
如果架构有
popcntl
。
C++ 编译器并未编写这种高度优化的代码。你是。
如果您开始在 C++ 中使用一些与 Haskell 中相同类型的库,您会发现它不再生成整洁且简短的汇编:
#include <gmpxx.h>
#include <string>
int main(int argc, char* argv[]) {
mpz_class r{0};
mpz_class c{0};
for(char c : std::string(argv[1])) {
r *= 10;
r += c - '0';
}
r *= 2;
return r.get_ui();
}
同时,如果你开始像编写 C++ 一样编写 Haskell(这确实不是我的强项,但这是我的第一次尝试):
foo :: Ptr CChar -> Int
foo !ptr = unsafePerformIO $ do
n <- f 0 ptr
return $! n*2
where
f :: Int -> Ptr CChar -> IO Int
f !n !ptr = do
c <- peek ptr
if c == 0 then return n else f (n*10+(fromEnum c - 0x30)) (plusPtr ptr 1)
然后你会发现 GHC 还生成更严格的代码:
406c48: add r12,0x10
406c4c: cmp r12,QWORD PTR [r13+0x358]
406c53: ja 406c8a <Main_zdwf_info+0x42>
406c55: movsx rax,BYTE PTR [rsi] # c = *s
406c59: test rax,rax # if c == 0
406c5c: je 406c75 <Main_zdwf_info+0x2d> # goto done
406c5e: add r12,0xfffffffffffffff0
406c62: inc rsi # s++
406c65: imul r14,r14,0xa # r *= 10
406c69: add r14,rax
406c6c: lea rax,[r14-0x30] # tmp += c - '0'
406c70: mov r14,rax # r = tmp
406c73: jmp 406c48 <Main_zdwf_info> # goto start