Skip to content

Commit ce9916f

Browse files
committed
Speed up strict and lazy search
1 parent 2f90469 commit ce9916f

File tree

4 files changed

+114
-108
lines changed

4 files changed

+114
-108
lines changed

src/Data/Text.hs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1684,10 +1684,10 @@ findIndex p t = S.findIndex p (stream t)
16841684
-- In (unlikely) bad cases, this function's time complexity degrades
16851685
-- towards /O(n*m)/.
16861686
count :: Text -> Text -> Int
1687-
count pat src
1687+
count pat
16881688
| null pat = emptyError "count"
1689-
| isSingleton pat = countChar (unsafeHead pat) src
1690-
| otherwise = L.length (indices pat src)
1689+
| isSingleton pat = countChar (unsafeHead pat)
1690+
| otherwise = L.length . indices pat
16911691
{-# INLINE [1] count #-}
16921692

16931693
{-# RULES

src/Data/Text/Internal/Lazy/Search.hs

Lines changed: 61 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
{-# LANGUAGE BangPatterns, ScopedTypeVariables #-}
2+
{-# LANGUAGE MagicHash #-}
3+
{-# LANGUAGE UnliftedFFITypes #-}
24

35
-- |
46
-- Module : Data.Text.Lazy.Search
@@ -27,9 +29,14 @@ import qualified Data.Text.Array as A
2729
import Data.Int (Int64)
2830
import Data.Word (Word8, Word64)
2931
import qualified Data.Text.Internal as T
32+
import qualified Data.Text as T (concat)
3033
import Data.Text.Internal.Fusion.Types (PairS(..))
31-
import Data.Text.Internal.Lazy (Text(..), foldlChunks)
34+
import Data.Text.Internal.Lazy (Text(..), foldrChunks, equal)
35+
import Data.Text.Unsafe (unsafeDupablePerformIO)
3236
import Data.Bits ((.|.), (.&.))
37+
import Foreign.C.Types
38+
import GHC.Exts (ByteArray#)
39+
import System.Posix.Types (CSsize(..))
3340

3441
-- | /O(n+m)/ Find the offsets of all non-overlapping indices of
3542
-- @needle@ within @haystack@.
@@ -42,110 +49,90 @@ import Data.Bits ((.|.), (.&.))
4249
indices :: Text -- ^ Substring to search for (@needle@)
4350
-> Text -- ^ Text to search in (@haystack@)
4451
-> [Int64]
45-
indices needle@(Chunk n ns) _haystack@(Chunk k ks)
46-
| nlen <= 0 = []
47-
| nlen == 1 = indicesOne (nindex 0) 0 k ks
48-
| otherwise = advance k ks 0 0
52+
indices needle
53+
| nlen <= 0 = const []
54+
| nlen == 1 = indicesOne (A.unsafeIndex narr noff) 0
55+
| otherwise = advance 0 0
4956
where
50-
advance x@(T.Text _ _ l) xs = scan
51-
where
52-
scan !g !i
53-
| i >= m = case xs of
54-
Empty -> []
55-
Chunk y ys -> advance y ys g (i-m)
57+
T.Text narr noff nlen = T.concat (foldrChunks (:) [] needle)
58+
59+
advance !_ !_ Empty = []
60+
advance !(g :: Int64) !(i :: Int) xxs@(Chunk x@(T.Text xarr@(A.ByteArray xarr#) xoff l) xs)
61+
| i >= l = advance g (i - l) xs
5662
| lackingHay (i + nlen) x xs = []
57-
| c == z && candidateMatch 0 = g : scan (g+nlen) (i+nlen)
58-
| otherwise = scan (g+delta) (i+delta)
63+
| c == z && candidateMatch = g : advance (g + intToInt64 nlen) (i + nlen) xxs
64+
| otherwise = advance (g + intToInt64 delta) (i + delta) xxs
5965
where
60-
m = intToInt64 l
61-
c = hindex (i + nlast)
66+
c = index xxs (i + nlast)
6267
delta | nextInPattern = nlen + 1
6368
| c == z = skip + 1
64-
| otherwise = 1
65-
nextInPattern = mask .&. swizzle (hindex (i+nlen)) == 0
66-
candidateMatch !j
67-
| j >= nlast = True
68-
| hindex (i+j) /= nindex j = False
69-
| otherwise = candidateMatch (j+1)
70-
hindex = index x xs
71-
nlen = wordLength needle
69+
| l >= i + nlen = case unsafeDupablePerformIO $
70+
memchr xarr# (intToCSize (xoff + i + nlen)) (intToCSize (l - i - nlen)) z of
71+
-1 -> max 1 (l - i - nlen)
72+
s -> cSsizeToInt s + 1
73+
| otherwise = 1
74+
nextInPattern = mask .&. swizzle (index xxs (i + nlen)) == 0
75+
76+
candidateMatch
77+
| i + nlen <= l = A.equal narr noff xarr (xoff + i) nlen
78+
| otherwise = A.equal narr noff xarr (xoff + i) (l - i) &&
79+
Chunk (T.Text narr (noff + l - i) (nlen - l + i)) Empty `equal` xs
80+
7281
nlast = nlen - 1
73-
nindex = index n ns
74-
z = foldlChunks fin 0 needle
75-
where fin _ (T.Text farr foff flen) = A.unsafeIndex farr (foff+flen-1)
76-
(mask :: Word64) :*: skip = buildTable n ns 0 0 0 (nlen-2)
82+
z = A.unsafeIndex narr (noff + nlen - 1)
83+
(mask :: Word64) :*: skip = buildTable 0 0 0 (nlen-2)
7784

7885
swizzle :: Word8 -> Word64
7986
swizzle w = 1 `unsafeShiftL` (word8ToInt w .&. 0x3f)
8087

81-
buildTable (T.Text xarr xoff xlen) xs = go
82-
where
83-
go !(g::Int64) !i !msk !skp
84-
| i >= xlast = case xs of
85-
Empty -> (msk .|. swizzle z) :*: skp
86-
Chunk y ys -> buildTable y ys g 0 msk' skp'
87-
| otherwise = go (g+1) (i+1) msk' skp'
88-
where c = A.unsafeIndex xarr (xoff+i)
88+
buildTable !g !i !msk !skp
89+
| i >= nlast = (msk .|. swizzle z) :*: skp
90+
| otherwise = buildTable (g+1) (i+1) msk' skp'
91+
where c = A.unsafeIndex narr (noff+i)
8992
msk' = msk .|. swizzle c
9093
skp' | c == z = nlen - g - 2
9194
| otherwise = skp
92-
xlast = xlen - 1
9395

9496
-- | Check whether an attempt to index into the haystack at the
9597
-- given offset would fail.
96-
lackingHay :: Int64 -> T.Text -> Text -> Bool
97-
lackingHay q = go 0
98-
where
99-
go p (T.Text _ _ l) ps = p' < q && case ps of
100-
Empty -> True
101-
Chunk r rs -> go p' r rs
102-
where p' = p + intToInt64 l
103-
indices _ _ = []
98+
lackingHay :: Int -> T.Text -> Text -> Bool
99+
lackingHay q (T.Text _ _ l) ps = l < q && case ps of
100+
Empty -> True
101+
Chunk r rs -> lackingHay (q - l) r rs
104102

105103
-- | Fast index into a partly unpacked 'Text'. We take into account
106104
-- the possibility that the caller might try to access one element
107105
-- past the end.
108-
index :: T.Text -> Text -> Int64 -> Word8
109-
index (T.Text arr off len) xs !i
110-
| j < len = A.unsafeIndex arr (off+j)
111-
| otherwise = case xs of
112-
Empty
113-
-- out of bounds, but legal
114-
| j == len -> 0
115-
-- should never happen, due to lackingHay above
116-
| otherwise -> emptyError "index"
117-
Chunk c cs -> index c cs (i-intToInt64 len)
118-
where j = int64ToInt i
106+
index :: Text -> Int -> Word8
107+
index Empty !_ = 0
108+
index (Chunk (T.Text arr off len) xs) !i
109+
| i < len = A.unsafeIndex arr (off + i)
110+
| otherwise = index xs (i - len)
119111

120112
-- | A variant of 'indices' that scans linearly for a single 'Word8'.
121-
indicesOne :: Word8 -> Int64 -> T.Text -> Text -> [Int64]
113+
indicesOne :: Word8 -> Int64 -> Text -> [Int64]
122114
indicesOne c = chunk
123115
where
124-
chunk :: Int64 -> T.Text -> Text -> [Int64]
125-
chunk !i (T.Text oarr ooff olen) os = go 0
116+
chunk :: Int64 -> Text -> [Int64]
117+
chunk !_ Empty = []
118+
chunk !i (Chunk (T.Text oarr ooff olen) os) = go 0
126119
where
127-
go h | h >= olen = case os of
128-
Empty -> []
129-
Chunk y ys -> chunk (i+intToInt64 olen) y ys
120+
go h | h >= olen = chunk (i+intToInt64 olen) os
130121
| on == c = i + intToInt64 h : go (h+1)
131122
| otherwise = go (h+1)
132123
where on = A.unsafeIndex oarr (ooff+h)
133124

134-
-- | The number of 'Word8' values in a 'Text'.
135-
wordLength :: Text -> Int64
136-
wordLength = foldlChunks sumLength 0
137-
where
138-
sumLength :: Int64 -> T.Text -> Int64
139-
sumLength i (T.Text _ _ l) = i + intToInt64 l
140-
141-
emptyError :: String -> a
142-
emptyError fun = error ("Data.Text.Lazy.Search." ++ fun ++ ": empty input")
143-
144125
intToInt64 :: Int -> Int64
145126
intToInt64 = fromIntegral
146127

147-
int64ToInt :: Int64 -> Int
148-
int64ToInt = fromIntegral
149-
150128
word8ToInt :: Word8 -> Int
151129
word8ToInt = fromIntegral
130+
131+
intToCSize :: Int -> CSize
132+
intToCSize = fromIntegral
133+
134+
cSsizeToInt :: CSsize -> Int
135+
cSsizeToInt = fromIntegral
136+
137+
foreign import ccall unsafe "_hs_text_memchr" memchr
138+
:: ByteArray# -> CSize -> CSize -> Word8 -> IO CSsize

src/Data/Text/Internal/Search.hs

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
{-# LANGUAGE BangPatterns, ScopedTypeVariables #-}
2+
{-# LANGUAGE MagicHash #-}
3+
{-# LANGUAGE UnliftedFFITypes #-}
24

35
-- |
46
-- Module : Data.Text.Internal.Search
@@ -35,6 +37,10 @@ import qualified Data.Text.Array as A
3537
import Data.Word (Word64, Word8)
3638
import Data.Text.Internal (Text(..))
3739
import Data.Bits ((.|.), (.&.), unsafeShiftL)
40+
import Data.Text.Unsafe (unsafeDupablePerformIO)
41+
import Foreign.C.Types
42+
import GHC.Exts (ByteArray#)
43+
import System.Posix.Types (CSsize(..))
3844

3945
data T = {-# UNPACK #-} !Word64 :* {-# UNPACK #-} !Int
4046

@@ -48,47 +54,60 @@ data T = {-# UNPACK #-} !Word64 :* {-# UNPACK #-} !Int
4854
indices :: Text -- ^ Substring to search for (@needle@)
4955
-> Text -- ^ Text to search in (@haystack@)
5056
-> [Int]
51-
indices _needle@(Text narr noff nlen) _haystack@(Text harr hoff hlen)
52-
| nlen == 1 = scanOne (nindex 0)
53-
| nlen <= 0 || ldiff < 0 = []
54-
| otherwise = scan 0
57+
indices (Text narr noff nlen)
58+
| nlen == 1 = scanOne (A.unsafeIndex narr noff)
59+
| nlen <= 0 = const []
60+
| otherwise = scan
5561
where
56-
ldiff = hlen - nlen
5762
nlast = nlen - 1
58-
z = nindex nlast
63+
!z = nindex nlast
5964
nindex k = A.unsafeIndex narr (noff+k)
60-
hindex k = A.unsafeIndex harr (hoff+k)
61-
hindex' k | k == hlen = 0
62-
| otherwise = A.unsafeIndex harr (hoff+k)
6365
buildTable !i !msk !skp
6466
| i >= nlast = (msk .|. swizzle z) :* skp
6567
| otherwise = buildTable (i+1) (msk .|. swizzle c) skp'
66-
where c = nindex i
68+
where !c = nindex i
6769
skp' | c == z = nlen - i - 2
6870
| otherwise = skp
71+
!(mask :* skip) = buildTable 0 0 (nlen-2)
6972

7073
swizzle :: Word8 -> Word64
71-
swizzle k = 1 `unsafeShiftL` (word8ToInt k .&. 0x3f)
74+
swizzle !k = 1 `unsafeShiftL` (word8ToInt k .&. 0x3f)
7275

73-
scan !i
74-
| i > ldiff = []
75-
| c == z && candidateMatch 0 = i : scan (i + nlen)
76-
| otherwise = scan (i + delta)
77-
where c = hindex (i + nlast)
78-
candidateMatch !j
79-
| j >= nlast = True
80-
| hindex (i+j) /= nindex j = False
81-
| otherwise = candidateMatch (j+1)
82-
delta | nextInPattern = nlen + 1
83-
| c == z = skip + 1
84-
| otherwise = 1
85-
where nextInPattern = mask .&. swizzle (hindex' (i+nlen)) == 0
86-
!(mask :* skip) = buildTable 0 0 (nlen-2)
87-
scanOne c = loop 0
88-
where loop !i | i >= hlen = []
89-
| hindex i == c = i : loop (i+1)
90-
| otherwise = loop (i+1)
76+
scan (Text harr@(A.ByteArray harr#) hoff hlen) = loop (hoff + nlen) where
77+
loop !i
78+
| i > hlen + hoff
79+
= []
80+
| A.unsafeIndex harr (i - 1) == z
81+
= if A.equal narr noff harr (i - nlen) nlen
82+
then i - nlen : loop (i + nlen)
83+
else loop (i + skip + 1)
84+
| i == hlen + hoff
85+
= []
86+
| mask .&. swizzle (A.unsafeIndex harr i) == 0
87+
= loop (i + nlen + 1)
88+
| otherwise
89+
= case unsafeDupablePerformIO $ memchr harr# (intToCSize i) (intToCSize (hlen + hoff - i)) z of
90+
-1 -> []
91+
x -> loop (i + cSsizeToInt x + 1)
9192
{-# INLINE indices #-}
9293

94+
scanOne :: Word8 -> Text -> [Int]
95+
scanOne c (Text harr hoff hlen) = loop 0
96+
where
97+
loop !i
98+
| i >= hlen = []
99+
| A.unsafeIndex harr (hoff+i) == c = i : loop (i+1)
100+
| otherwise = loop (i+1)
101+
{-# INLINE scanOne #-}
102+
93103
word8ToInt :: Word8 -> Int
94104
word8ToInt = fromIntegral
105+
106+
intToCSize :: Int -> CSize
107+
intToCSize = fromIntegral
108+
109+
cSsizeToInt :: CSsize -> Int
110+
cSsizeToInt = fromIntegral
111+
112+
foreign import ccall unsafe "_hs_text_memchr" memchr
113+
:: ByteArray# -> CSize -> CSize -> Word8 -> IO CSsize

src/Data/Text/Lazy.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1599,9 +1599,9 @@ index t n = S.index (stream t) n
15991599
-- In (unlikely) bad cases, this function's time complexity degrades
16001600
-- towards /O(n*m)/.
16011601
count :: Text -> Text -> Int64
1602-
count pat src
1602+
count pat
16031603
| null pat = emptyError "count"
1604-
| otherwise = go 0 (indices pat src)
1604+
| otherwise = go 0 . indices pat
16051605
where go !n [] = n
16061606
go !n (_:xs) = go (n+1) xs
16071607
{-# INLINE [1] count #-}

0 commit comments

Comments
 (0)