AtCoderの過去問をF#で解いていてTLEに長時間悩まされたのでメモ。
int32では合算値が桁あふれしてしまう場合の対策として次のような自作関数を定義して使っていた。
let bigSum (arr : int []) =
let rec loop (i : int) (sum : int64) =
if i < 0 then sum
else (sum + int64 arr.[i]) |> loop (i - 1)
loop (arr.Length - 1) (int64 0)
しかし (sum + int64 arr.[i]) |> loop (i - 1)
の書き方では10の5乗の要素があるような配列を与えた際に極端に速度が遅くなりTLEしてしまう。
let bigSum (arr : int []) =
let rec loop (i : int) (sum : int64) =
if i < 0 then sum
- else (sum + int64 arr.[i]) |> loop (i - 1)
+ else loop (i - 1) (sum + int64 arr.[i])
loop (arr.Length - 1) (int64 0)
このように書き直すと改善された。
それ以上の検証はしていないが、おそらくパイプライン演算子を用いた前者の書き方では末尾再帰最適化か効かないということだと推測している。
原因の突き止めにはコードテストが非常に役立った。
こちらに下のコードを貼って実行すると、
open System
let bigSum (arr : int []) =
let rec loop (i : int) (sum : int64) =
if i < 0 then sum
else loop (i - 1) (sum + int64 arr.[i]) // 末尾再帰
loop (arr.Length - 1) (int64 0)
[<EntryPoint>]
let main _argv =
let n = 10.0 ** 5.0 |> int
let aList =
[ for _ in 0..n - 1 -> 10.0 ** 9.0 |> int ]
|> List.toArray
bigSum aList |> printfn "%d"
0 // return an int exit code
終了コード 0
実行時間 60 ms
という結果になる。
一方、問題の書き方をすると、
open System
let bigSum (arr : int []) =
let rec loop (i : int) (sum : int64) =
if i < 0 then sum
else sum + int64 arr.[i] |> loop (i - 1) // 末尾再帰最適化が効かない?
loop (arr.Length - 1) (int64 0)
[<EntryPoint>]
let main _argv =
let n = 10.0 ** 5.0 |> int
let aList =
[ for _ in 0..n - 1 -> 10.0 ** 9.0 |> int ]
|> List.toArray
bigSum aList |> printfn "%d"
0 // return an int exit code
終了コード 9
実行時間 10508 ms
となる。
手元の端末だと後者のコードでも一瞬で処理が終わったのですぐ原因がわからなかった。
動作環境のスペックの違いによるものだと思うが、手元環境では .NetCore SDK 2.2.401
AtCoderでは Mono4.0
というランタイムの違いもあり、ひょっとしたらこちらということもあるかもしれない。