@@ -52,90 +52,6 @@ but may suffer from insufficient inlining, allocating a tree of thunks. It is st
5252significantly faster than ` Data.Text.Lazy.Builder ` , as witnessed by benchmarks
5353for ` blaze-builder ` below.
5454
55- ## Case study
56-
57- Let's benchmark builders, which concatenate all ` Char ` from ` minBound ` to ` maxBound ` , producing a large ` Text ` :
58-
59- ``` haskell
60- #!/usr/bin/env cabal
61- {- cabal:
62- build-depends: base, tasty-bench, text, text-builder >= 1.0, text-builder-linear
63- ghc-options: -O2
64- -}
65-
66- import qualified Data.Text as T
67- import qualified Data.Text.Lazy as TL
68- import qualified Data.Text.Lazy.Builder as TLB
69- import qualified TextBuilder as TB
70- import qualified Data.Text.Builder.Linear as TBL
71- import System.Environment (getArgs )
72- import Test.Tasty.Bench
73-
74- mkBench :: Monoid a => String -> (Char -> a ) -> (a -> Int ) -> Benchmark
75- mkBench s f g = bench s $ nf (g . foldMap f . enumFromTo minBound ) maxBound
76- {-# INLINE mkBench #-}
77-
78- main :: IO ()
79- main = defaultMain
80- [ mkBench " text, lazy" TLB. singleton (fromIntegral . TL. length . TLB. toLazyText)
81- , mkBench " text, strict" TLB. singleton (T. length . TL. toStrict . TLB. toLazyText)
82- , mkBench " text-builder" TB. char (T. length . TB. toText)
83- , mkBench " text-builder-linear" TBL. fromChar (T. length . TBL. runBuilder)
84- ]
85- ```
86-
87- Running this program with ` cabal run Main.hs -- +RTS -T ` yields following results:
88-
89- ```
90- text, lazy:
91- 4.25 ms ± 107 μs, 11 MB allocated, 912 B copied
92- text, strict:
93- 7.18 ms ± 235 μs, 24 MB allocated, 10 MB copied
94- text-builder:
95- 80.1 ms ± 3.0 ms, 218 MB allocated, 107 MB copied
96- text-builder-linear:
97- 5.37 ms ± 146 μs, 44 MB allocated, 78 KB copied
98- ```
99-
100- The first result seems the best both in time and memory and corresponds to the
101- usual ` Text ` builder, where we do not materialize the entire result at all.
102- It builds chunks of lazy ` Text ` lazily and consumes them at once by
103- ` TL.length ` . Thus there are 11 MB of allocations in nursery, none of which
104- survive generation 0 garbage collector, so nothing is copied.
105-
106- The second result is again the usual ` Text ` builder, but emulates a strict
107- consumer: we materialize a strict ` Text ` before computing length. Allocation
108- are doubled, and half of them (corresponding to the strict ` Text ` ) survive to
109- the heap. Time is also almost twice longer, but still quite good.
110-
111- The third result is for ` text-builder ` and demonstrates how bad things could
112- go with strict builders, aiming to precompute the precise length of the
113- buffer: allocating a thunk per char is tremendously slow and expensive.
114-
115- The last result corresponds to the current package. We generate a strict
116- ` Text ` by growing and reallocating the buffer, thus allocations are quite
117- high. Nevertheless, it is already faster than the usual ` Text ` builder with
118- strict consumer and does not strain the garbage collector.
119-
120- As of GHC 9.10, things get very different if we remove ` {-# INLINE mkBench #-} ` :
121-
122- ```
123- text, lazy:
124- 36.9 ms ± 599 μs, 275 MB allocated, 30 KB copied
125- text, strict:
126- 44.7 ms ± 1.3 ms, 287 MB allocated, 25 MB copied
127- text-builder:
128- 77.6 ms ± 2.2 ms, 218 MB allocated, 107 MB copied
129- text-builder-linear:
130- 5.35 ms ± 212 μs, 44 MB allocated, 79 KB copied
131- ```
132-
133- Builders from ` text ` package degrade rapidly, 6-8x slower and 10-20x more
134- allocations. That's because their constant factors rely crucially on
135- everything getting inlined, which makes their performance fragile and
136- unreliable in large-scale applications. On the bright side of things, our
137- builder remains as fast as before and now is a clear champion.
138-
13955## Benchmarks for ` Text `
14056
14157Measured with GHC 9.12 on aarch64:
0 commit comments