为什么这个 Haskell 程序的程序集相当大。等效的 C 代码?或者我编译时做错了什么?

问题描述 投票:0回答:1

有一次我问为什么简单的“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 中表达的过程完全相同。但是考虑到我目前拥有的程序集的大小......它真的会产生很大的差异吗?

如果您认为是这样,请告诉我,我会删除这个问题并花时间尝试一下。


有些评论提到了类似的事情

  • 它包含一个运行时环境来确定要评估哪些thunk等。
  • 它不会显着增长,因为 haskel 需要大量的库和启动代码

但是为什么呢?我的意思是,

  • 该程序的作用很简单。编译器真的这么难解决吗?如果程序归结为如此简单的操作,为什么还需要 thunk?
  • 在我看来,必须达到代码量的临界值是非常“错误”的。

作为比较,当我读到(并验证)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 haskell assembly gcc ghc
1个回答
0
投票

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
© www.soinside.com 2019 - 2024. All rights reserved.