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

Haskell性能异常:同时运行两段代码耗时远高于单独运行

Haskell程序性能异常:单语句快、双语句慢的原因分析

问题背景

我写了一个Haskell程序,day11函数里包含这两行输出代码:

putStrLn $ "Day11: part1: " ++ show (sum $ bigManhattan 1 galaxies <$> pairs)
putStrLn $ "Day11: part2: " ++ show (sum $ bigManhattan 999999 galaxies <$> pairs)

单独注释掉任意一行时,程序运行仅需0.01秒;但同时保留两行,耗时直接飙升到90秒。想请教这是什么原因?会不会是两段代码访问数据时互相干扰?

编译选项如下:

ghc-options:
- -Wall
- -Wcompat
- -Widentities
- -Wincomplete-record-updates
- -Wincomplete-uni-patterns
- -Wmissing-export-lists
- -Wmissing-home-modules
- -Wpartial-fields
- -Wredundant-constraints
- -O2

使用GHC 9.4.7编译,完整代码:

module Day11(day11) where

import Data.List ((\\))
import Data.Maybe (catMaybes)


type Coord = (Int, Int)


manhattan :: Coord -> Coord -> Int
manhattan (x1, y1) (x2, y2) = abs (x1 - x2) + abs (y1 - y2)


getF :: (String -> a) -> Int -> IO a
getF f n = do
  s <- readFile $ "./Data/Day" ++ show n ++ ".in"
  return $ f s


getLines :: Int -> IO [String]
getLines = getF lines


parse :: [String] -> [Coord]
parse css = concatMap (catMaybes . (\(y, cs) -> (\(x, c) -> if c=='#' then Just (x,y) else Nothing) <$> zip [0..] cs)) (zip [0..] css)


nSize :: Int
nSize = 140


bigManhattan :: Int -> [Coord] -> (Coord, Coord) -> Int
bigManhattan k galaxies ((c1, r1), (c2, r2)) = manhattan (c1+newc1, r1+newr1) (c2+newc2, r2+newr2)
  where
    baseC, baseR :: [Int]
    baseC = [0..(nSize-1)] \\ (fst <$> galaxies)
    baseR = [0..(nSize-1)] \\  (snd <$> galaxies)
    newc1, newc2, newr1, newr2 :: Int
    newc1 = k * length (filter (c1>) baseC)
    newc2 = k * length (filter (c2>) baseC)
    newr1 = k * length (filter (r1>) baseR)
    newr2 = k * length (filter (r2>) baseR)


day11 :: IO ()
day11 = do
  ls <- getLines 11
  let galaxies = parse ls
      pairs = [(x,y) | x <- galaxies, y <- galaxies, x<y ]

  putStrLn $ "Day11: part1: " ++ show (sum $ bigManhattan 1 galaxies <$> pairs)
  --putStrLn $ "Day11: part2: " ++ show (sum $ bigManhattan 999999 galaxies <$> pairs)
  return ()

原因分析

核心问题出在bigManhattan函数的重复计算和GHC的优化策略差异上:

  • 重复计算baseC和baseR:每次调用bigManhattan,都会重新计算baseC和baseR(这两个值仅依赖galaxies,完全可以只算一次)。当前代码里这两个值定义在函数的where块中,意味着每处理一对星系就会重复计算一次,计算量随星系对数成倍数增长。
  • 单/双语句的优化差异:当只有一段计算时,GHC的-O2优化会自动做公共子表达式消除(CSE),把baseC/baseR的计算提升到循环外,避免重复计算。但同时存在两段计算时,GHC无法识别这两段代码中的baseC/baseR是相同的公共子表达式,导致每对星系的两次计算都要重复生成这两个列表,直接放大了重复计算的开销。
  • 不存在数据干扰:两段代码都是纯函数计算,没有共享可变状态,不存在“访问数据互相干扰”的问题,本质就是重复计算导致的性能灾难。

解决方案

把baseC和baseR提前计算好,传入bigManhattan,从根源上避免重复计算:

优化后的核心代码

-- 重构bigManhattan,提前传入预计算的baseC和baseR
bigManhattan :: Int -> [Int] -> [Int] -> (Coord, Coord) -> Int
bigManhattan k baseC baseR ((c1, r1), (c2, r2)) = manhattan (c1+newc1, r1+newr1) (c2+newc2, r2+newr2)
  where
    newc1 = k * length (filter (c1>) baseC)
    newc2 = k * length (filter (c2>) baseC)
    newr1 = k * length (filter (r1>) baseR)
    newr2 = k * length (filter (r2>) baseR)

day11 :: IO ()
day11 = do
  ls <- getLines 11
  let galaxies = parse ls
      pairs = [(x,y) | x <- galaxies, y <- galaxies, x<y ]
      -- 提前计算baseC和baseR,仅执行一次
      baseC = [0..(nSize-1)] \\ (fst <$> galaxies)
      baseR = [0..(nSize-1)] \\ (snd <$> galaxies)

  putStrLn $ "Day11: part1: " ++ show (sum $ bigManhattan 1 baseC baseR <$> pairs)
  putStrLn $ "Day11: part2: " ++ show (sum $ bigManhattan 999999 baseC baseR <$> pairs)
  return ()

进一步优化(可选)

还可以预计算每个星系坐标的偏移量,彻底避免在处理每对星系时调用filter和length:

-- 预计算每个坐标的偏移量
precomputeOffsets :: [Int] -> [Int] -> [Coord] -> [(Int, Int)]
precomputeOffsets baseC baseR galaxies = map (\(c,r) -> (length (filter (c>) baseC), length (filter (r>) baseR)) galaxies

day11 :: IO ()
day11 = do
  ls <- getLines 11
  let galaxies = parse ls
      pairs = [(x,y) | x <- galaxies, y <- galaxies, x<y ]
      baseC = [0..(nSize-1)] \\ (fst <$> galaxies)
      baseR = [0..(nSize-1)] \\ (snd <$> galaxies)
      offsets = precomputeOffsets baseC baseR galaxies
      -- 配对星系与偏移量
      galaxyWithOffsets = zip galaxies offsets
      -- 直接用预计算偏移量计算距离
      calcDist k (( (c1,r1), (oc1,or1) ), ( (c2,r2), (oc2,or2) )) = manhattan (c1 + k*oc1, r1 + k*or1) (c2 + k*oc2, r2 + k*or2)

  putStrLn $ "Day11: part1: " ++ show (sum $ calcDist 1 <$> pairs)
  putStrLn $ "Day11: part2: " ++ show (sum $ calcDist 999999 <$> pairs)
  return ()

优化后,不管是单语句还是双语句,运行速度都会保持在0.01秒左右。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 06:55:23