You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

将函数移至Lib模块后Haskell程序运行时长激增的原因及解决

Haskell跨模块迁移后性能暴跌的原因与解决方法

问题描述

我用stack new创建了一个Haskell模板项目,初始时area与sumOfAreas函数位于Main模块,在我的硬件上平均运行时长为100ms。将这两个函数迁移至Lib模块后,程序运行时长骤增至约1000ms。


项目代码与配置

初始代码(Main模块)

module Main where

import Lib
import           Data.Massiv.Array           (Sz (..), Vector)

import qualified Data.Massiv.Array           as A
import Data.Time.Clock.POSIX

t mul = round . (mul *) <$> getPOSIXTime
timeMillis = t 1000

area :: BBox -> Double
area (BBox (x1, y1) (x2, y2)) = (x2 - x1) * (y2 - y1)

sumOfAreas :: Vector A.DS BBox -> Double
sumOfAreas bboxes = A.sfoldl (\acc box -> acc + area box) 0 bboxes

main :: IO ()
main = do
    start <- timeMillis
    putStrLn $ "Start " ++ show start
    let
        bboxes = A.makeArray A.Seq (Sz 10000 * 10000) $ (\_ -> BBox (0, 0) (12, 12)) :: Vector A.DS BBox
        result = sumOfAreas bboxes
    putStrLn $ show result
    end <- timeMillis
    putStrLn $ show (end - start)

初始代码(Lib模块)

module Lib where

import           Data.Massiv.Array           (Vector)

import qualified Data.Massiv.Array           as A

type Point = (Double, Double)

data BBox =
  BBox (Point) (Point)
  deriving (Eq, Show)

迁移后代码(Main模块)

module Main where

import Lib
import           Data.Massiv.Array           (Sz (..), Vector)

import qualified Data.Massiv.Array           as A
import Data.Time.Clock.POSIX

t mul = round . (mul *) <$> getPOSIXTime
timeMillis = t 1000

main :: IO ()
main = do
    start <- timeMillis
    putStrLn $ "Start " ++ show start
    let
        bboxes = A.makeArray A.Seq (Sz 10000 * 10000) $ (\_ -> BBox (0, 0) (12, 12)) :: Vector A.DS BBox
        result = sumOfAreas bboxes
    putStrLn $ show result
    end <- timeMillis
    putStrLn $ show (end - start)

迁移后代码(Lib模块)

module Lib where

import           Data.Massiv.Array           (Vector)

import qualified Data.Massiv.Array           as A

type Point = (Double, Double)

data BBox =
  BBox (Point) (Point)
  deriving (Eq, Show)

area :: BBox -> Double
area (BBox (x1, y1) (x2, y2)) = (x2 - x1) * (y2 - y1)

sumOfAreas :: Vector A.DS BBox -> Double
sumOfAreas bboxes = A.sfoldl (\acc box -> acc + area box) 0 bboxes

Cabal配置文件

cabal-version: 1.12

-- This file has been generated from package.yaml by hpack version 0.35.0.
--
-- see: https://github.com/sol/hpack
-- ... info ...

source-repository head
  type: git
  location: https://github.com/githubuser/profiling

library
  exposed-modules:
      Lib
  other-modules:
      Paths_profiling
  hs-source-dirs:
      src
  ghc-options: -Wall -threaded -O3
  build-depends:
      base >=4.7 && <5
    , massiv ==1.0.1.1
    , vector ==0.12.3.1
    , array ==0.5.4.0
    , time ==1.9.3
  default-language: Haskell2010

executable profiling-exe
  main-is: Main.hs
  other-modules:
      Paths_profiling
  hs-source-dirs:
      app
  ghc-options: -Wall -threaded -O3
  build-depends:
      base >=4.7 && <5
    , massiv ==1.0.1.1
    , vector ==0.12.3.1
    , array ==0.5.4.0
    , time ==1.9.3
    , profiling
  default-language: Haskell2010

test-suite profiling-test
  type: exitcode-stdio-1.0
  main-is: Spec.hs
  other-modules:
      Paths_profiling
  hs-source-dirs:
      test
  ghc-options: -Wall -threaded -O3
  build-depends:
      base >=4.7 && <5
    , profiling
  default-language: Haskell2010

环境信息

  • Stack版本:
Version 2.9.1, Git revision 409d56031b4240221d656db09b2ba476fe6bb5b1 x86_64 hpack-0.35.0
  • GHC版本:9.2.4

原因分析

核心问题是跨模块函数内联被GHC优化器阻止:

  • 当area和sumOfAreas在Main模块时,GHC可以直接将area内联到sumOfAreas的折叠逻辑中,甚至进一步做常量折叠(所有BBox的面积都是固定的144,1亿个的总和是1.44×10¹⁰),几乎不需要实际遍历计算,所以运行极快。
  • 迁移到Lib模块后,默认情况下GHC不会跨模块自动内联普通函数,sumOfAreas里的匿名lambda无法直接嵌入area的逻辑,导致每次循环都要做一次函数调用,同时常量折叠优化也失效,最终性能暴跌10倍。

解决方法

方法1:显式添加内联标记

在Lib模块的area和sumOfAreas函数前加上INLINE编译指示,强制GHC跨模块内联这些函数:

module Lib where

import           Data.Massiv.Array           (Vector)
import qualified Data.Massiv.Array           as A

type Point = (Double, Double)

data BBox =
  BBox (Point) (Point)
  deriving (Eq, Show)

{-# INLINE area #-}
area :: BBox -> Double
area (BBox (x1, y1) (x2, y2)) = (x2 - x1) * (y2 - y1)

{-# INLINE sumOfAreas #-}
sumOfAreas :: Vector A.DS BBox -> Double
sumOfAreas bboxes = A.sfoldl (\acc box -> acc + area box) 0 bboxes

方法2:调整GHC跨模块优化参数

在Cabal配置的library部分添加更激进的内联参数,提升跨模块内联的阈值:

library
  # ... 其他配置不变 ...
  ghc-options: -Wall -threaded -O3 -funfolding-use-threshold=100 -funfolding-keeness-factor=100

不过显式添加INLINE标记的可靠性更高,不会影响其他函数的优化策略。

方法3:针对当前案例的特殊优化

由于所有BBox的面积都是常量,可以直接在数组生成阶段计算面积,完全避免后续的折叠循环:

-- 在Main模块或Lib模块中修改
sumOfAreas :: Int -> Double
sumOfAreas count = fromIntegral count * 144.0

-- main函数中直接使用
result = sumOfAreas (10000 * 10000)

这是针对当前场景的最优解,但仅适用于计算逻辑固定的情况。


忽略的关键要点

  1. 跨模块优化的保守性:GHC默认不会跨模块内联普通函数,必须显式标记或调整编译参数才能开启跨模块激进优化。
  2. 同模块优化的优势:同一模块内的函数可以被GHC做更多融合、常量折叠、循环展开等优化,跨模块时这些优化会被大幅限制。
  3. 性能敏感函数的标记:对于高频调用的小函数(如area这种简单计算函数),一定要显式添加INLINE标记,确保优化器能将逻辑嵌入到调用点,避免函数调用开销。

内容的提问来源于stack exchange,提问作者True Warg

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 01:15:44