on master help sign in
Improve small string performance for UTF-8 encoding to bytestrings

On a 5 byte string the conversion of strict text to a strict bytestring is still a factor 2x slower than the custom 'encodeUtf8_1' routine. However, this is much better than the factor 4.5x that we started with. I attribute the slowdown to the more expensive startup cost for the bytestring-builder-based solution. Note that this startup cost is shared in case a small string is encoded as part of a larger document, e.g., a JSON document. I am thus not sure how relevant the small string performance for converting to individual strict 'ByteString's is. Note that the ASCII performance of the Builder-based UTF-8 encoder is 1.6x faster than 'encodeUtf8_1'. The japanese and russion performance is about the same. Note also that the Builder-based strict text UTF-8 encoder has the benefit that it won't waste any memory. In contrast, the 'encodeUtf8_1' function can allocate as much as 4 times more memory than needed, as it does not trim the resulting bytestring.

commit1c65333713523fcd5c0f4c60a3c5d5b1134302e4 authorSimon Meier <simon.meier@erudify.com> dateTue 7 Jan 2014, 14:06 (13 years ago) parentfc217e7
2 files changed+30−7
Data/Text/Encoding.hs+17−6 Data/Text/Lazy/Encoding.hs+13−1
Data/Text/Encoding.hs+17−6
@@ -81,13 +81,11 @@ import Data.ByteString.Internal as B hiding (c2w)
8181
8282#if MIN_VERSION_bytestring(0,10,4)
8383import qualified Data.ByteString.Builder as B
84+import qualified Data.ByteString.Builder.Extra as B
8485import qualified Data.ByteString.Builder.Internal as B hiding (empty)
8586import qualified Data.ByteString.Builder.Prim as BP
8687import qualified Data.ByteString.Builder.Prim.Internal as BP
8788import qualified Data.ByteString.Lazy as BL
88−import Data.Text.Internal.Unsafe.Shift (shiftR)
89−#else
90−import Data.Text.Internal.Unsafe.Shift (shiftL, shiftR)
9189#endif
9290
9391#if __GLASGOW_HASKELL__ >= 706
@@ -99,6 +97,7 @@ import Foreign.ForeignPtr (unsafeForeignPtrToPtr)
9997import Data.Text ()
10098import Data.Text.Encoding.Error (OnDecodeError, UnicodeException, strictDecode)
10199import Data.Text.Internal (Text(..), safe, textP)
100+import Data.Text.Internal.Unsafe.Shift (shiftL, shiftR)
102101import Data.Text.Internal.Private (runText)
103102import Data.Text.Internal.Unsafe.Char (ord, unsafeWrite)
104103import Data.Word (Word8, Word32)
@@ -322,7 +321,17 @@ decodeUtf8' = unsafeDupablePerformIO . try . evaluate . decodeUtf8With strictDec
322321encodeUtf8 :: Text -> ByteString
323322#if MIN_VERSION_bytestring(0,10,4)
324323
325−encodeUtf8 = BL.toStrict . B.toLazyByteString . encodeUtf8Builder
324+encodeUtf8 t@(Text _arr _off len) =
325+ B.copy -- copy to trim and avoid wasting memory
326+ $ BL.toStrict
327+ $ B.toLazyByteStringWith strategy BL.empty
328+ $ encodeUtf8Builder t
329+ where
330+ -- We use a strategy that allocates a buffer that is guaranteed to be
331+ -- large enough for the whole result. This ensures that we always stay in
332+ -- the fast 'goPartial' loop in the 'encodeUtf8BuilderEscaped' function.
333+ strategy = B.untrimmedStrategy (4 * (len + 1)) B.defaultChunkSize
334+
326335
327336-- | Encode text to a ByteString 'B.Builder' using UTF-8 encoding.
328337encodeUtf8Builder :: Text -> B.Builder
@@ -351,6 +360,8 @@ encodeUtf8BuilderEscaped be =
351360 outerLoop !i0 !br@(B.BufferRange op0 ope)
352361 | i0 >= iend = k br
353362 | outRemaining > 0 = goPartial (i0 + min outRemaining inpRemaining)
363+ -- TODO: Use a loop with an integrated bound's check if outRemaining
364+ -- is smaller than 8, as this will save on divisions.
354365 | otherwise = return $ B.bufferFull bound op0 (outerLoop i0)
355366 where
356367 outRemaining = (ope `minusPtr` op0) `div` bound
@@ -387,6 +398,8 @@ encodeUtf8BuilderEscaped be =
387398
388399encodeUtf8 = encodeUtf8_0
389400
401+#endif
402+
390403encodeUtf8_0 :: Text -> ByteString
391404encodeUtf8_0 (Text arr off len) = unsafeDupablePerformIO $ do
392405 let size0 = max len 4
@@ -431,8 +444,6 @@ encodeUtf8_0 (Text arr off len) = unsafeDupablePerformIO $ do
431444 poke8 (m+2) $ (w .&. 0x3F) + 0x80
432445 go (n+1) (m+3)
433446
434−#endif
435−
436447encodeUtf8_1 :: Text -> ByteString
437448encodeUtf8_1 (Text arr off len)
438449 | len == 0 = B.empty
Data/Text/Lazy/Encoding.hs+13−1
@@ -64,7 +64,9 @@ import qualified Data.ByteString.Unsafe as B
6464import Data.Word (Word8)
6565import Data.Monoid (mempty, (<>))
6666import qualified Data.ByteString.Builder as B
67+import qualified Data.ByteString.Builder.Extra as B (safeStrategy, toLazyByteStringWith)
6768import qualified Data.ByteString.Builder.Prim as BP
69+import qualified Data.Text as T
6870#endif
6971import qualified Data.Text.Encoding as TE
7072import qualified Data.Text.Lazy as L
@@ -148,7 +150,17 @@ decodeUtf8' bs = unsafeDupablePerformIO $ do
148150
149151encodeUtf8 :: Text -> B.ByteString
150152#if MIN_VERSION_bytestring(0,10,4)
151−encodeUtf8 = B.toLazyByteString . encodeUtf8Builder
153+encodeUtf8 Empty = B.empty
154+encodeUtf8 lt@(Chunk t _) =
155+ B.toLazyByteStringWith strategy B.empty $ encodeUtf8Builder lt
156+ where
157+ -- To improve our small string performance, we use a strategy that
158+ -- allocates a buffer that is guaranteed to be large enough for the
159+ -- encoding of the first chunk, but not larger than the default
160+ -- B.smallChunkSize. We clamp the firstChunkSize to ensure that we don't
161+ -- generate too large buffers which hamper streaming.
162+ firstChunkSize = min B.smallChunkSize (4 * (T.length t + 1))
163+ strategy = B.safeStrategy firstChunkSize B.defaultChunkSize
152164
153165encodeUtf8Builder :: Text -> B.Builder
154166encodeUtf8Builder = foldrChunks (\c b -> TE.encodeUtf8Builder c <> b) mempty