1. Bryan O'Sullivan
  2. text


Simon Meier  committed c56c159

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

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.

  • Participants
  • Parent commits 22837f8
  • Branches default

Comments (0)

Files changed (2)

File Data/Text/Encoding.hs

View file
 #if MIN_VERSION_bytestring(0,10,4)
 import qualified Data.ByteString.Builder as B
+import qualified Data.ByteString.Builder.Extra    as B
 import qualified Data.ByteString.Builder.Internal as B hiding (empty)
 import qualified Data.ByteString.Builder.Prim as BP
 import qualified Data.ByteString.Builder.Prim.Internal as BP
 import qualified Data.ByteString.Lazy as BL
-import Data.Text.Internal.Unsafe.Shift (shiftR)
-import Data.Text.Internal.Unsafe.Shift (shiftL, shiftR)
 #if __GLASGOW_HASKELL__ >= 706
 import Data.Text ()
 import Data.Text.Encoding.Error (OnDecodeError, UnicodeException, strictDecode)
 import Data.Text.Internal (Text(..), safe, textP)
+import Data.Text.Internal.Unsafe.Shift (shiftL, shiftR)
 import Data.Text.Internal.Private (runText)
 import Data.Text.Internal.Unsafe.Char (ord, unsafeWrite)
 import Data.Word (Word8, Word32)
 encodeUtf8 :: Text -> ByteString
 #if MIN_VERSION_bytestring(0,10,4)
-encodeUtf8 = BL.toStrict . B.toLazyByteString . encodeUtf8Builder
+encodeUtf8 t@(Text _arr _off len) =
+      B.copy       -- copy to trim and avoid wasting memory
+    $ BL.toStrict
+    $ B.toLazyByteStringWith strategy BL.empty
+    $ encodeUtf8Builder t
+  where
+    -- We use a strategy that allocates a buffer that is guaranteed to be
+    -- large enough for the whole result. This ensures that we always stay in
+    -- the fast 'goPartial' loop in the 'encodeUtf8BuilderEscaped' function.
+    strategy = B.untrimmedStrategy (4 * (len + 1)) B.defaultChunkSize
 -- | Encode text to a ByteString 'B.Builder' using UTF-8 encoding.
 encodeUtf8Builder :: Text -> B.Builder
         outerLoop !i0 !br@(B.BufferRange op0 ope)
           | i0 >= iend       = k br
           | outRemaining > 0 = goPartial (i0 + min outRemaining inpRemaining)
+          -- TODO: Use a loop with an integrated bound's check if outRemaining
+          -- is smaller than 8, as this will save on divisions.
           | otherwise        = return $ B.bufferFull bound op0 (outerLoop i0)
             outRemaining = (ope `minusPtr` op0) `div` bound
 encodeUtf8 = encodeUtf8_0
 encodeUtf8_0 :: Text -> ByteString
 encodeUtf8_0 (Text arr off len) = unsafeDupablePerformIO $ do
   let size0 = max len 4
                   poke8 (m+2) $ (w .&. 0x3F) + 0x80
                   go (n+1) (m+3)
 encodeUtf8_1 :: Text -> ByteString
 encodeUtf8_1 (Text arr off len)
   | len == 0  = B.empty

File Data/Text/Lazy/Encoding.hs

View file
 import Data.Word (Word8)
 import Data.Monoid (mempty, (<>))
 import qualified Data.ByteString.Builder as B
+import qualified Data.ByteString.Builder.Extra as B (safeStrategy, toLazyByteStringWith)
 import qualified Data.ByteString.Builder.Prim as BP
+import qualified Data.Text as T
 import qualified Data.Text.Encoding as TE
 import qualified Data.Text.Lazy as L
 encodeUtf8 :: Text -> B.ByteString
 #if MIN_VERSION_bytestring(0,10,4)
-encodeUtf8 = B.toLazyByteString . encodeUtf8Builder
+encodeUtf8    Empty       = B.empty
+encodeUtf8 lt@(Chunk t _) =
+    B.toLazyByteStringWith strategy B.empty $ encodeUtf8Builder lt
+  where
+    -- To improve our small string performance, we use a strategy that
+    -- allocates a buffer that is guaranteed to be large enough for the
+    -- encoding of the first chunk, but not larger than the default
+    -- B.smallChunkSize. We clamp the firstChunkSize to ensure that we don't
+    -- generate too large buffers which hamper streaming.
+    firstChunkSize  = min B.smallChunkSize (4 * (T.length t + 1))
+    strategy        = B.safeStrategy firstChunkSize B.defaultChunkSize
 encodeUtf8Builder :: Text -> B.Builder
 encodeUtf8Builder = foldrChunks (\c b -> TE.encodeUtf8Builder c <> b) mempty