Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Let the list instance fuse #99

Merged
merged 2 commits into from
Jul 26, 2023
Merged

Let the list instance fuse #99

merged 2 commits into from
Jul 26, 2023

Conversation

Bodigrim
Copy link
Contributor

Fixes #76, CC @treeowl.

To measure performance of someFunc Haskell benchmarks often employ code like this:

map someFunc [1..1000] `deepseq` ()

Here we want to measure time of 1000 applications of someFunc, and in order to do so we deepseq the entire list map someFunc [1..1000]. However, in this scenario we are not really interested in materializing the list: if someFunc is relatively fast, allocations are likely to skew measurements. See Bodigrim/tasty-bench#48 (comment) for discussion and various hacky workarounds.

We can do better by making instance NFData [a] able to fuse by rewriting it via foldr. This has a nice side effect of avoiding manual recursion and having a more concise definition as well.

Before the patch

foo :: Int -> ()
foo n = [1..n] `deepseq` ()

compiles to

Rec {
$wgo3 :: [Int] -> (# #)
$wgo3
  = \ (ds_s8hJ :: [Int]) ->
      case ds_s8hJ of {
        [] -> (##);
        : y_i8gZ ys_i8h0 -> case y_i8gZ of { I# ipv_i8gR -> $wgo3 ys_i8h0 }
      }
end Rec }

$wfoo :: Int# -> (# #)
$wfoo
  = \ (ww_s8hT :: Int#) ->
      case ># 1# ww_s8hT of {
        __DEFAULT ->
          letrec {
            go3_a8hF :: Int# -> [Int]
            go3_a8hF
              = \ (x_a8hG :: Int#) ->
                  : (I# x_a8hG)
                    (case ==# x_a8hG ww_s8hT of {
                       __DEFAULT -> go3_a8hF (+# x_a8hG 1#);
                       1# -> []
                     }); } in
          $wgo3 (go3_a8hF 1#);
        1# -> (##)
      }

foo :: Int -> ()
foo
  = \ (n_s8hR :: Int) ->
      case n_s8hR of { I# ww_s8hT ->
      case $wfoo ww_s8hT of { (# #) -> () }
      }

Here one can observe $wgo3 which is forcing a list (essentially this is rnf from instance NFData [a]) and go3_a8hF which produces it. Such code allocates boxed Int and (:) constructors.

After the patch:

foo :: Int -> ()
foo
  = \ (n_a8g8 :: Int) ->
      case n_a8g8 of { I# y_a8ha ->
      case ># 1# y_a8ha of {
        __DEFAULT ->
          joinrec {
            go3_a5N1 :: Int# -> ()
            go3_a5N1 (x_a5N2 :: Int#)
              = case ==# x_a5N2 y_a8ha of {
                  __DEFAULT -> jump go3_a5N1 (+# x_a5N2 1#);
                  1# -> ()
                }; } in
          jump go3_a5N1 1#;
        1# -> ()
      }
      }

Here we do force evaluation of all elements of the list, but do not allocate them.

To measure performance of `someFunc` Haskell benchmarks often employ code like this:

```haskell
map someFunc [1..1000] `deepseq` ()
```

Here we want to measure time of 1000 applications of `someFunc`, and in order to do so we `deepseq` the entire list `map someFunc [1..1000]`. However, in this scenario we are not really interested in materializing the list: if `someFunc` is relatively fast, allocations are likely to skew measurements. See Bodigrim/tasty-bench#48 (comment) for discussion and various hacky workarounds.

We can do better by making `instance NFData [a]` able to fuse by rewriting it via `foldr`. This has a nice side effect of avoiding manual recursion and having a more concise definition as well.

Before the patch

```haskell
foo :: Int -> ()
foo n = [1..n] `deepseq` ()
```

compiles to

```haskell
Rec {
$wgo3 :: [Int] -> (# #)
$wgo3
  = \ (ds_s8hJ :: [Int]) ->
      case ds_s8hJ of {
        [] -> (##);
        : y_i8gZ ys_i8h0 -> case y_i8gZ of { I# ipv_i8gR -> $wgo3 ys_i8h0 }
      }
end Rec }

$wfoo :: Int# -> (# #)
$wfoo
  = \ (ww_s8hT :: Int#) ->
      case ># 1# ww_s8hT of {
        __DEFAULT ->
          letrec {
            go3_a8hF :: Int# -> [Int]
            go3_a8hF
              = \ (x_a8hG :: Int#) ->
                  : (I# x_a8hG)
                    (case ==# x_a8hG ww_s8hT of {
                       __DEFAULT -> go3_a8hF (+# x_a8hG 1#);
                       1# -> []
                     }); } in
          $wgo3 (go3_a8hF 1#);
        1# -> (##)
      }

foo :: Int -> ()
foo
  = \ (n_s8hR :: Int) ->
      case n_s8hR of { I# ww_s8hT ->
      case $wfoo ww_s8hT of { (# #) -> () }
      }
```

Here one can observe `$wgo3` which is forcing a list (essentially this is `rnf` from `instance NFData [a]`) and `go3_a8hF` which produces it. Such code allocates boxed `Int` and `(:)` constructors.

After the patch:

```haskell
foo :: Int -> ()
foo
  = \ (n_a8g8 :: Int) ->
      case n_a8g8 of { I# y_a8ha ->
      case ># 1# y_a8ha of {
        __DEFAULT ->
          joinrec {
            go3_a5N1 :: Int# -> ()
            go3_a5N1 (x_a5N2 :: Int#)
              = case ==# x_a5N2 y_a8ha of {
                  __DEFAULT -> jump go3_a5N1 (+# x_a5N2 1#);
                  1# -> ()
                }; } in
          jump go3_a5N1 1#;
        1# -> ()
      }
      }
```

Here we do force evaluation of all elements of the list, but do not allocate them.
changelog.md Outdated Show resolved Hide resolved
@mixphix mixphix merged commit ec7de61 into master Jul 26, 2023
12 checks passed
@Bodigrim Bodigrim deleted the fuse-list-instance branch March 17, 2024 00:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Let the list instance fuse
2 participants