byteslice-0.2.13.1: src/Data/Bytes/Search.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RankNTypes #-}
-- This is broken out into a separate module to make it easier
-- to dump core and investigate performance issues.
module Data.Bytes.Search
( findIndices
, replace
, isInfixOf
) where
import Prelude hiding (all, any, dropWhile, elem, foldl, foldr, length, map, null, readFile, replicate, takeWhile)
import Control.Monad.ST.Run (runByteArrayST, runPrimArrayST)
import Data.Bits (finiteBitSize, shiftL, (.&.), (.|.))
import Data.Bytes.Pure (length, unsafeHead, unsafeIndex)
import Data.Bytes.Types (Bytes (Bytes, array, offset))
import Data.Primitive (ByteArray, PrimArray)
import GHC.Exts (Int (I#))
import GHC.Word (Word32)
import qualified Data.Bytes.Byte as Byte
import qualified Data.Bytes.Pure as Pure
import qualified Data.Bytes.Types as Types
import qualified Data.Primitive as PM
-- Implementation Notes
-- =====================
-- For karp rabin, there are some easy performance improvements
-- left on the table. The main optimization that has been done is making
-- sure that there is no unnecessary boxing of Int, Word32, or Bytes
-- going on. Here are some other things that have not been done:
--
-- * The hash is currently a Word32. It would be better to use either
-- Word or Word64 for this. We would need for hashKey to be different.
-- * In several places, we track an index into a Bytes. This index gets
-- repeatedly added to the base offset as we loop over the bytes. We
-- could instead track the true offset instead of repeatedly
-- recalculating it.
{- | Replace every non-overlapping occurrence of @needle@ in
@haystack@ with @replacement@.
-}
replace ::
-- | needle, must not be empty
Bytes ->
-- | replacement
Bytes ->
-- | haystack
Bytes ->
Bytes
{-# NOINLINE replace #-}
-- Implementation note: there is a lot of room to improve the performance
-- of this function.
replace !needle !replacement !haystack@Bytes {array = haystackArray, offset = haystackIndex, length = haystackLength}
| Pure.length needle == 0 = errorWithoutStackTrace "Data.Bytes.replace: needle of length zero"
| Pure.length haystack == 0 = Pure.empty
| Pure.length needle == 1
, Pure.length replacement == 1 =
let !needle0 = unsafeIndex needle 0
!replacement0 = unsafeIndex replacement 0
in Pure.map (\w -> if w == needle0 then replacement0 else w) haystack
| otherwise =
let !hp = rollingHash needle
!ixs = findIndicesKarpRabin 0 hp needle haystackArray haystackIndex haystackLength
in Pure.fromByteArray (replaceIndices ixs replacement (Pure.length needle) haystackArray haystackIndex haystackLength)
-- This is an internal function because it deals explicitly with
-- an offset into a byte array.
--
-- Example:
-- * haystack len: 39
-- * ixs: 7, 19, 33
-- * patLen: 5
-- * replacment: foo (len 3)
-- We want to perform these copies:
-- * src[0,7] -> dst[0,7]
-- * foo -> dst[7,3]
-- * src[12,7] -> dst[10,7]
-- * foo -> dst[17,3]
-- * src[24,9] -> dst[20,9]
-- * foo -> dst[29,3]
-- * src[38,1] -> dst[32,1]
replaceIndices :: PrimArray Int -> Bytes -> Int -> ByteArray -> Int -> Int -> ByteArray
replaceIndices !ixs !replacement !patLen !haystack !ix0 !len0 = runByteArrayST $ do
let !ixsLen = PM.sizeofPrimArray ixs
let !delta = Pure.length replacement - patLen
dst <- PM.newByteArray (len0 + ixsLen * delta)
let applyReplacement !ixIx !prevSrcIx =
if ixIx < ixsLen
then do
let !srcMatchIx = PM.indexPrimArray ixs ixIx
let !offset = ixIx * delta
let !dstIx = srcMatchIx + offset - ix0
Pure.unsafeCopy
dst
(prevSrcIx + offset - ix0)
Bytes {array = haystack, offset = prevSrcIx, length = srcMatchIx - prevSrcIx}
Pure.unsafeCopy dst dstIx replacement
applyReplacement (ixIx + 1) (srcMatchIx + patLen)
else do
let !offset = ixIx * delta
Pure.unsafeCopy
dst
(prevSrcIx + offset - ix0)
Bytes {array = haystack, offset = prevSrcIx, length = (len0 + ix0) - prevSrcIx}
PM.unsafeFreezeByteArray dst
applyReplacement 0 ix0
-- | Find locations of non-overlapping instances of @needle@ within @haystack@.
findIndices ::
-- | needle
Bytes ->
-- | haystack
Bytes ->
PrimArray Int
findIndices needle Bytes {array, offset = off, length = len}
| needleLen == 0 = errorWithoutStackTrace "Data.Bytes.findIndices: needle with length zero"
| len == 0 = mempty
| otherwise =
let !hp = rollingHash needle
in findIndicesKarpRabin (negate off) hp needle array off len
where
needleLen = Pure.length needle
-- Precondition: Haystack has non-zero length
-- Precondition: Pattern has non-zero length
-- Uses karp rabin to search.
-- Easy opportunity to improve implementation. Instead of having karpRabin
-- return two slices, we could have it just return a single index.
findIndicesKarpRabin ::
Int -> -- Output index modifier. Set to negated initial index to make slicing invisible in results.
Word32 -> -- Hash to search for (must agree with pattern)
Bytes -> -- Pattern to search for
ByteArray ->
Int -> -- initial index
Int -> -- length
PrimArray Int
findIndicesKarpRabin !ixModifier !hp !pat !haystack !ix0 !len0 = runPrimArrayST $ do
let dstLen = 1 + quot len0 (Pure.length pat)
dst <- PM.newPrimArray dstLen
let go !ix !len !ixIx = case karpRabin hp pat Bytes {array = haystack, offset = ix, length = len} of
(-1) -> do
PM.shrinkMutablePrimArray dst ixIx
PM.unsafeFreezePrimArray dst
skipCount -> do
let !advancement = skipCount - Pure.length pat
let !advancement' = advancement + Pure.length pat
PM.writePrimArray dst ixIx (ix + advancement + ixModifier)
let !ix' = ix + advancement'
go ix' (len - advancement') (ixIx + 1)
go ix0 len0 0
-- Output: Negative one means match not found. Other negative
-- numbers should not occur. Zero may occur. Positive number
-- means the number of bytes to skip to make it past the match.
breakSubstring ::
-- | String to search for
Bytes ->
-- | String to search in
Bytes ->
Int
breakSubstring !pat !haystack@(Bytes _ off0 _) =
case lp of
0 -> 0
1 -> case Byte.elemIndexLoop# (unsafeHead pat) haystack of
(-1#) -> (-1)
off -> 1 + (I# off) - off0
_ ->
if lp * 8 <= finiteBitSize (0 :: Word)
then shift haystack
else karpRabin (rollingHash pat) pat haystack
where
lp = length pat
{-# INLINE shift #-}
shift :: Bytes -> Int
shift !src
| length src < lp = (-1)
| otherwise = search (intoWord $ Pure.unsafeTake lp src) lp
where
intoWord :: Bytes -> Word
intoWord = Pure.foldl' (\w b -> (w `shiftL` 8) .|. fromIntegral b) 0
wp = intoWord pat
mask = (1 `shiftL` (8 * lp)) - 1
search :: Word -> Int -> Int
search !w !i
| w == wp = i
| length src <= i = (-1)
| otherwise = search w' (i + 1)
where
b = fromIntegral (Pure.unsafeIndex src i)
w' = mask .&. ((w `shiftL` 8) .|. b)
-- Only used for karp rabin
rollingHash :: Bytes -> Word32
{-# INLINE rollingHash #-}
rollingHash = Pure.foldl' (\h b -> h * hashKey + fromIntegral b) 0
hashKey :: Word32
{-# INLINE hashKey #-}
hashKey = 2891336453
-- Precondition: Length of bytes is greater than or equal to 1.
-- Precondition: Rolling hash agrees with pattern.
-- Output: Negative one means match not found. Other negative
-- numbers should not occur. Zero should not occur. Positive number
-- means the number of bytes to skip to make it past the match.
karpRabin :: Word32 -> Bytes -> Bytes -> Int
karpRabin !hp !pat !src
| length src < lp = (-1)
| otherwise = search (rollingHash $ Pure.unsafeTake lp src) lp
where
lp :: Int
!lp = Pure.length pat
m :: Word32
!m = hashKey ^ lp
get :: Int -> Word32
get !ix = fromIntegral (Pure.unsafeIndex src ix)
search !hs !i
| hp == hs && eqBytesNoShortCut pat (Pure.unsafeTake lp (Pure.unsafeDrop (i - lp) src)) = i
| length src <= i = (-1)
| otherwise = search hs' (i + 1)
where
hs' =
hs * hashKey
+ get i
- m * get (i - lp)
{- | Is the first argument an infix of the second argument?
Uses the Rabin-Karp algorithm: expected time @O(n+m)@, worst-case @O(nm)@.
-}
isInfixOf ::
-- | String to search for
Bytes ->
-- | String to search in
Bytes ->
Bool
isInfixOf p s = Pure.null p || breakSubstring p s >= 0
-- Precondition: both arguments have the same length
-- Skips the pointer equality check and the length check.
eqBytesNoShortCut :: Bytes -> Bytes -> Bool
{-# INLINE eqBytesNoShortCut #-}
eqBytesNoShortCut (Bytes arr1 off1 len1) (Bytes arr2 off2 _) =
PM.compareByteArrays arr1 off1 arr2 off2 len1 == EQ