-
Notifications
You must be signed in to change notification settings - Fork 157
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
A better Data.Text.Lazy.inits
#562
Comments
|
Sure, I considered it a given that we want to preserve laziness. More specifically, we preserve the chunk boundaries in the result. Also, for now, I haven't distinguished between length and chunk length for simplicity. In the worst case they are equal. Of course, we want to consider this if it matters between competing implementations.
How?
Benchmarks of some use casesimport Test.Tasty.Bench
import Data.List
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Internal.Lazy as TL
main :: IO ()
main = defaultMain
[ env (pure t_) $ \t ->
bgroup "lazy text"
[ bench "inits" $ nf TL.inits t
, bench "last . inits" $ nf (last . TL.inits) t
, bench "take 1 . last . inits" $ nf (TL.take 1 . last . TL.inits) t
, bench "map (take 1) . inits" $ nf (map (TL.take 1) . TL.inits) t
]
, env (pure xs_) $ \xs ->
bgroup "list"
[ bench "inits" $ nf inits xs
, bench "last . inits" $ nf (last . inits) xs
, bench "take 1 . last . inits" $ nf (take 1 . last . inits) xs
, bench "map (take 1) . inits" $ nf (map (take 1) . inits) xs
]
]
where
n = 1000
t_ :: TL.Text
t_ = foldr TL.Chunk TL.empty $ replicate n $ T.singleton 'a'
xs_ :: [Int]
xs_ = [1..n]
All of these except for |
Well, if you insist that the chunk boundaries in in the i-th output
As mentioned, the basic idea is to reduce the total number of chunks in the output by concatenating input chunks. I haven't worked through all of the details, but here's a (rather long, sorry) sketch: Upper bound:
When following this strategy:
Lower bound:
For simplicity I will only consider the case where numChunks is n and every chunk has size 1.
Assign a weight of 1/i to the i-th character from the end of any LazyText in the output (one-indexed). The total of these weights is equal to the sum of the first n harmonic numbers and is roughly n*ln(n)+O(n). It turns out that for any chunk-buffer used in the output, the total weight of all the characters in the output that are provided by that chunk-buffer is at most the cost of creating and using that chunk-buffer. Suppose the chunk-buffer has length p>0 and is used q>0 times. Because we are properly lazy, we cannot start using this chunk-buffer before its last character is supposed to have been forced. So in particular the last character in the chunk-buffer's i-th appearance has a weight of at most 1/i. And the j-th-to-last character in the i-th appearance has a weight of at most 1/(i+j-1), one-indexing again. There is at most one valid (i,j) pair that adds up to 2, and these have total weight of at most 1. There are at most two valid (i,j) pairs that add up to 3 and these also have total weight at most 1. Generally there are at most k-1 valid (i,j) pairs that add up to k, and for any single sum k they provide a total weight of at most 1. Since the valid sums range between 2 and p+q inclusive, this means the total weight provided by a given chunk-buffer is at most p+q-1. If p>1, then we paid a cost of p to create the chunk-buffer by concatenation for a total cost of p+q, which is greater than our upper bound for the weight. Otherwise, p=1 so that p+q-1=q which is exactly our total cost. I think the same basic idea should work for smaller numbers of roughly-equally-large chunks, giving each input-chunk equivalent in an output LazyText a weight of 1/(numLaterInputChunkEquivalents+1), but the details of the final calculation will be much messier.
Yeah, I remembered the problem of |
A better implementation of I am unsure about an implementation that concatenates chunks to produce later prefixes faster. For one thing, it can have a negative effect on lazy texts consisting of few big chunks, where the work of re-chaining chunks together is negligible compared to concatenating them. That is arguably the common case. |
Is using a queue or a difference list as in OldList and bytestring worth it? Instead isn't it sufficient to keep only the length as an accumulator, so each prefix can simply be generated as
|
That seems much simpler! It needs to be modified to support infinite lists however. I traced the OldList implementation to https://gitlab.haskell.org/ghc/ghc/-/issues/9345. This The same might not apply to lazy text though, so it is worth evaluating. |
Thanks for finding that thread! @treeowl might you remember why the queue implementation of I also dug up the libraries thread https://mail.haskell.org/pipermail/libraries/2014-July/023328.html |
@Lysxia , it was chosen because it was (surprisingly to me) faster. As I recall, someone who knows more than I do speculated that this had to do with the way GHC represents closures. |
I was also quite surprised for a while but I found a simple explanation. I was wrongly assuming that Okasaki's banker's queue was somehow incurring unnecessary overhead. In fact, To confirm this, I managed to write an even faster Implementation and benchmarking https://gist.github.com/Lysxia/3b733d87da8ea056c4e4a27fc047e170 Benchmark results ( Plain text output:
|
|
It is lazy like the others. It uses unsafe primitives but it is entirely invisible for a user of |
My apologies. I missed one of the |
Ah, right... |
#558 made me notice that while the implementation of$O(n^2)$ ), it has an awkward property that evaluating the ith text in the result takes $O(i^2)$ time. Ideally this would just take $O(i)$ time, since it involves reaching the ith element in a list and the element is a text of length i.
inits
is overall optimal (I believe
Data.List.inits
has solved this problem using a queue: https://hackage.haskell.org/package/base-4.19.0.0/docs/src/Data.OldList.html#initsThe same could be adopted here.
The text was updated successfully, but these errors were encountered: