diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
 # Revision history for byteslice
 
+## 0.2.2.0 -- 2020-02-27
+
+* Add `split4`.
+* Add `equalsCString`.
+* Add `stripCStringPrefix`.
+* Add `equalsLatin8`.
+* Add `emptyPinned`.
+* Add `concatPinned` to `Data.Bytes.Chunks`.
+* Add `any` and `all`.
+
 ## 0.2.1.0 -- 2020-01-22
 
 * Add `longestCommonPrefix`.
diff --git a/bench/Main.hs b/bench/Main.hs
new file mode 100644
--- /dev/null
+++ b/bench/Main.hs
@@ -0,0 +1,17 @@
+import Gauge.Main (defaultMain, bench, whnf)
+
+import Data.Bytes.Types
+import Data.List (permutations)
+import qualified Data.Bytes as Bytes
+
+naiveMconcat :: [Bytes] -> Bytes
+naiveMconcat = foldr mappend mempty
+
+main :: IO ()
+main = defaultMain
+  [ bench "mconcat" $ whnf mconcat mconcatBytes
+  , bench "naiveMconcat" $ whnf naiveMconcat mconcatBytes
+  ]
+
+mconcatBytes :: [Bytes]
+mconcatBytes = fmap Bytes.fromAsciiString $ permutations ['a'..'g']
diff --git a/byteslice.cabal b/byteslice.cabal
--- a/byteslice.cabal
+++ b/byteslice.cabal
@@ -1,6 +1,6 @@
 cabal-version: 2.2
 name: byteslice
-version: 0.2.1.0
+version: 0.2.2.0
 synopsis: Slicing managed and unmanaged memory
 description:
   This library provides types that allow the user to talk about a slice of
@@ -54,3 +54,15 @@
     , tasty
     , tasty-hunit
     , tasty-quickcheck
+
+benchmark bench
+  type: exitcode-stdio-1.0
+  build-depends:
+    , base
+    , byteslice
+    , gauge
+    , primitive
+  ghc-options: -Wall -O2
+  default-language: Haskell2010
+  hs-source-dirs: bench
+  main-is: Main.hs
diff --git a/src/Data/Bytes.hs b/src/Data/Bytes.hs
--- a/src/Data/Bytes.hs
+++ b/src/Data/Bytes.hs
@@ -10,12 +10,16 @@
     Bytes
     -- * Constants
   , empty
+  , emptyPinned
     -- * Properties
   , null
   , length
     -- * Decompose
   , uncons
   , unsnoc
+    -- * Predicates
+  , any
+  , all
     -- * Create
     -- ** Sliced
   , singleton
@@ -50,6 +54,9 @@
   , split1
   , split2
   , split3
+  , split4
+    -- * Combining
+  , intercalate
     -- * Counting
   , Byte.count
     -- * Prefix and Suffix
@@ -61,10 +68,13 @@
   , stripSuffix
   , stripOptionalSuffix
   , longestCommonPrefix
+    -- ** C Strings
+  , stripCStringPrefix
     -- ** Single Byte
   , isBytePrefixOf
   , isByteSuffixOf
     -- * Equality
+    -- ** Fixed Characters
   , equalsLatin1
   , equalsLatin2
   , equalsLatin3
@@ -72,6 +82,9 @@
   , equalsLatin5
   , equalsLatin6
   , equalsLatin7
+  , equalsLatin8
+    -- ** C Strings
+  , equalsCString
     -- * Unsafe Slicing
   , unsafeTake
   , unsafeDrop
@@ -94,14 +107,15 @@
   , hPut
   ) where
 
-import Prelude hiding (length,takeWhile,dropWhile,null,foldl,foldr,elem,replicate)
+import Prelude hiding (length,takeWhile,dropWhile,null,foldl,foldr,elem,replicate,any,all)
 
 import Control.Monad.Primitive (PrimMonad,PrimState,primitive_,unsafeIOToPrim)
 import Control.Monad.ST.Run (runByteArrayST)
 import Data.Bytes.Types (Bytes(Bytes,array,offset))
 import Data.Char (ord)
 import Data.Primitive (ByteArray(ByteArray),MutableByteArray)
-import Foreign.Ptr (Ptr,plusPtr)
+import Foreign.C.String (CString)
+import Foreign.Ptr (Ptr,plusPtr,castPtr)
 import GHC.Exts (Int(I#),Char(C#),word2Int#,chr#)
 import GHC.Exts (Word#,Int#)
 import GHC.IO (IO(IO))
@@ -109,7 +123,10 @@
 import System.IO (Handle)
 
 import qualified Data.Bytes.Byte as Byte
+import qualified Data.Foldable as F
+import qualified Data.List as List
 import qualified Data.Primitive as PM
+import qualified Data.Primitive.Ptr as PM
 import qualified GHC.Exts as Exts
 import qualified System.IO as IO
 
@@ -317,6 +334,33 @@
             , Bytes arr (k + 1) (len - (1 + k - off))
             )
 
+-- | Split a byte sequence on the first, second, third, and fourth
+-- occurrences of the target byte. The target is removed from the result.
+-- For example:
+--
+-- >>> split4 0xA [0x1,0x2,0xA,0xB,0xA,0xA,0xA]
+-- Just ([0x1,0x2],[0xB],[],[],[])
+split4 :: Word8 -> Bytes -> Maybe (Bytes,Bytes,Bytes,Bytes,Bytes)
+{-# inline split4 #-}
+split4 w b@(Bytes arr off len) = case elemIndexLoop# w b of
+  (-1#) -> Nothing
+  i# -> let i = I# i# in
+    case elemIndexLoop# w (Bytes arr (i + 1) (len - (1 + i - off))) of
+      (-1#) -> Nothing
+      j# -> let j = I# j# in
+        case elemIndexLoop# w (Bytes arr (j + 1) (len - (1 + j - off))) of
+          (-1#) -> Nothing
+          k# -> let k = I# k# in
+            case elemIndexLoop# w (Bytes arr (k + 1) (len - (1 + k - off))) of
+              (-1#) -> Nothing
+              m# -> let m = I# m# in Just
+                ( Bytes arr off (i - off)
+                , Bytes arr (i + 1) (j - (i + 1))
+                , Bytes arr (j + 1) (k - (j + 1))
+                , Bytes arr (k + 1) (m - (k + 1))
+                , Bytes arr (m + 1) (len - (1 + m - off))
+                )
+
 -- This returns the offset into the byte array. This is not an index
 -- that will mean anything to the end user, so it cannot be returned
 -- to them.
@@ -565,6 +609,44 @@
        c6 == indexCharArray arr (off + 6)
   _ -> False
 
+-- | Is the byte sequence, when interpreted as ISO-8859-1-encoded text,
+-- an octupleton whose elements match the characters?
+equalsLatin8 :: Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Bytes -> Bool
+equalsLatin8 !c0 !c1 !c2 !c3 !c4 !c5 !c6 !c7 (Bytes arr off len) = case len of
+  8 -> c0 == indexCharArray arr off &&
+       c1 == indexCharArray arr (off + 1) &&
+       c2 == indexCharArray arr (off + 2) &&
+       c3 == indexCharArray arr (off + 3) &&
+       c4 == indexCharArray arr (off + 4) &&
+       c5 == indexCharArray arr (off + 5) &&
+       c6 == indexCharArray arr (off + 6) &&
+       c7 == indexCharArray arr (off + 7)
+  _ -> False
+
+-- | Is the byte sequence equal to the @NUL@-terminated C String?
+-- The C string must be a constant.
+equalsCString :: CString -> Bytes -> Bool
+{-# inline equalsCString #-}
+equalsCString !ptr0 (Bytes arr off0 len0) = go (castPtr ptr0 :: Ptr Word8) off0 len0 where
+  go !ptr !off !len = case len of
+    0 -> PM.indexOffPtr ptr 0 == (0 :: Word8)
+    _ -> case PM.indexOffPtr ptr 0 of
+      0 -> False
+      c -> c == PM.indexByteArray arr off && go (plusPtr ptr 1) (off + 1) (len - 1)
+
+-- | /O(n)/ Variant of 'stripPrefix' that takes a @NUL@-terminated C String
+-- as the prefix to test for.
+stripCStringPrefix :: CString -> Bytes -> Maybe Bytes
+{-# inline stripCStringPrefix #-}
+stripCStringPrefix !ptr0 (Bytes arr off0 len0) = go (castPtr ptr0 :: Ptr Word8) off0 len0 where
+  go !ptr !off !len = case PM.indexOffPtr ptr 0 of
+    0 -> Just (Bytes arr off len)
+    c -> case len of
+      0 -> Nothing
+      _ -> case c == PM.indexByteArray arr off of
+        True -> go (plusPtr ptr 1) (off + 1) (len - 1)
+        False -> Nothing
+
 -- | Copy the byte sequence into a mutable buffer. The buffer must have
 -- enough space to accomodate the byte sequence, but this this is not
 -- checked.
@@ -610,6 +692,13 @@
 empty :: Bytes
 empty = Bytes mempty 0 0
 
+-- | The empty byte sequence.
+emptyPinned :: Bytes
+emptyPinned = Bytes
+  ( runByteArrayST
+    (PM.newPinnedByteArray 0 >>= PM.unsafeFreezeByteArray)
+  ) 0 0
+
 -- | Read 'Bytes' directly from the specified 'Handle'. The resulting
 -- 'Bytes' are pinned. This is implemented with 'hGetBuf'.
 hGet :: Handle -> Int -> IO Bytes
@@ -629,6 +718,7 @@
 createPinnedAndTrim maxSz f = do
   arr@(PM.MutableByteArray arr#) <- PM.newPinnedByteArray maxSz
   sz <- f (PM.mutableByteArrayContents arr)
+  touchMutableByteArrayIO arr
   PM.shrinkMutablePrimArray (PM.MutablePrimArray @Exts.RealWorld @Word8 arr#) sz
   r <- PM.unsafeFreezeByteArray arr
   pure (Bytes r 0 sz)
@@ -636,3 +726,40 @@
 touchByteArrayIO :: ByteArray -> IO ()
 touchByteArrayIO (ByteArray x) =
   IO (\s -> (# Exts.touch# x s, () #))
+
+touchMutableByteArrayIO :: MutableByteArray s -> IO ()
+touchMutableByteArrayIO (PM.MutableByteArray x) =
+  IO (\s -> (# Exts.touch# x s, () #))
+
+-- | /O(n)/ The intercalate function takes a separator 'Bytes' and a list of
+-- 'Bytes' and concatenates the list elements by interspersing the separator
+-- between each element.
+intercalate ::
+     Bytes -- ^ Separator (interspersed into the list)
+  -> [Bytes] -- ^ List
+  -> Bytes
+intercalate !_ [] = mempty
+intercalate !_ [x] = x
+intercalate (Bytes sarr soff slen) (Bytes arr0 off0 len0 : bs) = Bytes r 0 fullLen
+  where
+  !fullLen = List.foldl' (\acc (Bytes _ _ len) -> acc + len + slen) 0 bs + len0
+  r = runByteArrayST $ do
+    marr <- PM.newByteArray fullLen
+    PM.copyByteArray marr 0 arr0 off0 len0
+    !_ <- F.foldlM
+      (\ !currLen (Bytes arr off len) -> do
+        PM.copyByteArray marr currLen sarr soff slen
+        PM.copyByteArray marr (currLen + slen) arr off len
+        pure (currLen + len + slen)
+      ) len0 bs
+    PM.unsafeFreezeByteArray marr
+
+-- | /O(n)/ Returns true if any byte in the sequence satisfies the predicate.
+any :: (Word8 -> Bool) -> Bytes -> Bool
+{-# inline any #-}
+any f = foldr (\b r -> f b || r) False
+
+-- | /O(n)/ Returns true if all bytes in the sequence satisfy the predicate.
+all :: (Word8 -> Bool) -> Bytes -> Bool
+{-# inline all #-}
+all f = foldr (\b r -> f b && r) True
diff --git a/src/Data/Bytes/Chunks.hs b/src/Data/Bytes/Chunks.hs
--- a/src/Data/Bytes/Chunks.hs
+++ b/src/Data/Bytes/Chunks.hs
@@ -5,6 +5,7 @@
 {-# language MagicHash #-}
 {-# language UnboxedTuples #-}
 {-# language NamedFieldPuns #-}
+{-# language RankNTypes #-}
 
 -- | Chunks of bytes. This is useful as a target for a builder
 -- or as a way to read a large amount of whose size is unknown
@@ -19,6 +20,7 @@
   , length
     -- * Manipulate
   , concat
+  , concatPinned
   , concatU
   , reverse
   , reverseOnto
@@ -67,6 +69,16 @@
   -- it is tedious.
   a == b = concat a == concat b
 
+-- | Variant of 'concat' that ensure that the resulting byte
+-- sequence is pinned memory.
+concatPinned :: Chunks -> Bytes
+concatPinned x = case x of
+  ChunksNil -> Bytes.emptyPinned
+  ChunksCons b y -> case y of
+    ChunksNil -> Bytes.pin b
+    ChunksCons c z -> case concatPinnedFollowing2 b c z of
+      (# len, r #) -> Bytes (ByteArray r) 0 (I# len)
+
 -- | Concatenate chunks into a single contiguous byte sequence.
 concat :: Chunks -> Bytes
 concat x = case x of
@@ -86,13 +98,25 @@
       (# _, r #) -> ByteArray r
 
 concatFollowing2 :: Bytes -> Bytes -> Chunks -> (# Int#, ByteArray# #)
-concatFollowing2
+concatFollowing2 = internalConcatFollowing2 PM.newByteArray
+
+concatPinnedFollowing2 :: Bytes -> Bytes -> Chunks -> (# Int#, ByteArray# #)
+concatPinnedFollowing2 = internalConcatFollowing2 PM.newPinnedByteArray
+
+internalConcatFollowing2 ::
+     (forall s. Int -> ST s (MutableByteArray s))
+  -> Bytes
+  -> Bytes
+  -> Chunks
+  -> (# Int#, ByteArray# #)
+{-# inline internalConcatFollowing2 #-}
+internalConcatFollowing2 allocate
   (Bytes{array=c,offset=coff,length=szc}) 
   (Bytes{array=d,offset=doff,length=szd}) ds =
     let !(I# x, ByteArray y) = runIntByteArrayST $ do
           let !szboth = szc + szd
               !len = chunksLengthGo szboth ds
-          dst <- PM.newByteArray len
+          dst <- allocate len
           PM.copyByteArray dst 0 c coff szc
           PM.copyByteArray dst szc d doff szd
           -- Note: len2 will always be the same as len.
diff --git a/src/Data/Bytes/Types.hs b/src/Data/Bytes/Types.hs
--- a/src/Data/Bytes/Types.hs
+++ b/src/Data/Bytes/Types.hs
@@ -10,16 +10,18 @@
   ) where
 
 import Control.Monad.ST (runST)
-import Data.Primitive (ByteArray(..),MutableByteArray(..))
-import Data.Primitive.Addr (Addr)
+import Control.Monad.ST.Run (runByteArrayST)
 import Data.Bits ((.&.),unsafeShiftR)
 import Data.Char (ord)
+import Data.Primitive (ByteArray(..),MutableByteArray(..))
+import Data.Primitive.Addr (Addr)
 import Data.Word (Word8)
 import GHC.Base (unsafeChr)
 import GHC.Exts (Int(I#),unsafeCoerce#,sameMutableByteArray#)
 import GHC.Exts (isTrue#,compareByteArrays#,IsList(..))
 
 import qualified Data.List as L
+import qualified Data.Foldable as F
 import qualified Data.Primitive as PM
 
 -- | A slice of a 'ByteArray'.
@@ -98,6 +100,19 @@
 
 instance Monoid Bytes where
   mempty = Bytes mempty 0 0
+  mconcat [] = mempty
+  mconcat [x] = x
+  mconcat bs = Bytes r 0 fullLen
+    where
+    !fullLen = L.foldl' (\acc (Bytes _ _ len) -> acc + len) 0 bs
+    r = runByteArrayST $ do
+      marr <- PM.newByteArray fullLen
+      !_ <- F.foldlM
+        (\ !currLen (Bytes arr off len) -> do
+          PM.copyByteArray marr currLen arr off len
+          pure (currLen + len)
+        ) 0 bs
+      PM.unsafeFreezeByteArray marr
 
 compareByteArrays :: ByteArray -> Int -> ByteArray -> Int -> Int -> Ordering
 {-# INLINE compareByteArrays #-}
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -120,6 +120,10 @@
       ByteString.split x (ByteString.pack xs)
       ===
       map bytesToByteString (Bytes.split x (slicedPack xs))
+  , testProperty "intercalate" $ \(x :: Bytes) (xs :: [Bytes]) ->
+      mconcat (List.intersperse x xs)
+      ===
+      Bytes.intercalate x xs
   , testProperty "splitNonEmpty" $ \(x :: Word8) (xs :: [Word8]) ->
       Bytes.split x (slicedPack xs)
       ===
@@ -163,6 +167,12 @@
       ==>
       case Bytes.split3 0xEF (slicedPack (ws ++ [0xEF] ++ xs ++ [0xEF] ++ ys ++ [0xEF] ++ zs)) of
         Just r -> r === (slicedPack ws, slicedPack xs, slicedPack ys, slicedPack zs)
+        Nothing -> property False
+  , testProperty "split4" $ \(ws :: [Word8]) (xs :: [Word8]) (ys :: [Word8]) (zs :: [Word8]) (ps :: [Word8]) ->
+      (all (/=0xEF) ws && all (/=0xEF) xs && all (/=0xEF) ys && all (/=0xEF) zs && all (/=0xEF) ps)
+      ==>
+      case Bytes.split4 0xEF (slicedPack (ws ++ [0xEF] ++ xs ++ [0xEF] ++ ys ++ [0xEF] ++ zs ++ [0xEF] ++ ps)) of
+        Just r -> r === (slicedPack ws, slicedPack xs, slicedPack ys, slicedPack zs, slicedPack ps)
         Nothing -> property False
   , testGroup "Chunks"
     [ lawsToTest (QCC.eqLaws (Proxy :: Proxy Chunks))
