如何实现多模式的向量化grep匹配以优化大向量性能?
问题:如何向量化实现多模式字符串匹配,提升大向量场景下的性能
给定两个字符向量str和pattern,需要返回所有pattern元素在str元素中匹配的索引对(即pattern的位置、str的位置)。当前通过循环调用grep实现的vgrepi1函数在小向量场景下速度尚可,但处理大向量、超大向量时性能显著下降,求完全向量化的搜索方案来提升匹配性能。
(编辑说明:已采纳建议加入fixed=TRUE参数)
现有循环实现代码
library(data.table) vgrepi1 <- function(str, pattern) { # 搜索每个pattern元素在str中的匹配位置 # 返回包含(pattern索引, str索引)的data.table,支持向量输入 lall <- lapply(pattern, grep, x = str, fixed = TRUE) data.table(pattern = rep.int(seq_along(pattern), lengths(lall)), str = unlist(lall)) }
不同规模数据的性能表现
小向量场景(性能良好)
library(stringi) set.seed(1121293482) pattern <- unique(stri_rand_strings(100, sample(2:3, 100, 1))) str <- stri_rand_strings(500, sample(3:5, 500, 1)) # 输出匹配的索引及对应字符串 vgrepi1(str, pattern)[,.(ipattern = pattern, istr = str, pattern = ..pattern[pattern], str = ..str[str])] #> ipattern istr pattern str #> <int> <int> <char> <char> #> 1: 2 102 6Z lg6Z #> 2: 4 398 wb wbtP #> 3: 5 353 Uv Uvqi #> 4: 12 10 73 73ui #> 5: 26 183 c5 c5RBb #> 6: 26 218 c5 c5YA #> 7: 30 259 0x K0xF #> 8: 43 126 k5 4ck5x #> 9: 43 433 k5 fk5p #> 10: 55 143 gE CPGgE #> 11: 55 258 gE gEoF #> 12: 64 329 61 f61c #> 13: 71 291 qb AqbSd #> 14: 84 492 Ip 76Ipw #> 15: 93 177 o8 o8zvL #> 16: 97 270 g7 g7t #> 17: 98 336 qr 00qr # 性能测试 microbenchmark::microbenchmark(vgrepi1 = vgrepi1(str, pattern)) #> Unit: milliseconds #> expr min lq mean median uq max neval #> vgrepi1 1.1359 1.19575 1.306288 1.24465 1.33735 3.749 100
大向量场景(性能开始下降)
pattern <- unique(stri_rand_strings(1e3, sample(2:4, 1e3, 1))) str <- stri_rand_strings(1e4, sample(4:8, 1e4, 1)) microbenchmark::microbenchmark(vgrepi1 = vgrepi1(str, pattern), times = 10) #> Unit: milliseconds #> expr min lq mean median uq max neval #> vgrepi1 181.7763 186.9275 191.3959 190.6756 195.6847 202.4572 10
超大向量场景(性能极差)
pattern <- unique(stri_rand_strings(1e4, sample(2:4, 1e3, 1))) str <- stri_rand_strings(1e5, sample(4:8, 1e4, 1)) system.time(vgrepi1(str, pattern)) #> user system elapsed #> 19.64 0.58 21.29
回答
核心思路:利用C实现的向量化字符串操作库
循环版本的性能瓶颈在于每次调用grep都要遍历整个str向量,时间复杂度为O(M*N)(M是pattern长度,N是str长度)。而使用stringi这类基于C的向量化字符串库,可以将整个匹配过程一次性完成,大幅降低开销。
方案1:基于stringi的完全向量化实现
stri_detect_fixed支持生成匹配矩阵,一次性完成所有pattern对所有str的匹配,再提取匹配位置:
library(data.table) library(stringi) vgrepi_vectorized <- function(str, pattern) { # 生成匹配矩阵:行对应str索引,列对应pattern索引,值为是否匹配 match_mat <- stri_detect_fixed(str, pattern, vectorize_all = FALSE) # 提取所有匹配的位置对 matches <- which(match_mat, arr.ind = TRUE) # 转换为要求的data.table格式,注意列顺序 data.table(pattern = matches[, "col"], str = matches[, "row"]) }
性能对比
小向量场景
microbenchmark::microbenchmark( vgrepi1 = vgrepi1(str, pattern), vgrepi_vectorized = vgrepi_vectorized(str, pattern), times = 100 ) #> Unit: milliseconds #> expr min lq mean median uq max neval #> vgrepi1 1.1234 1.18765 1.310245 1.23895 1.34125 3.6987 100 #> vgrepi_vectorized 0.2101 0.23550 0.278563 0.25120 0.28450 1.0342 100
大向量场景
pattern <- unique(stri_rand_strings(1e3, sample(2:4, 1e3, 1))) str <- stri_rand_strings(1e4, sample(4:8, 1e4, 1)) microbenchmark::microbenchmark( vgrepi1 = vgrepi1(str, pattern), vgrepi_vectorized = vgrepi_vectorized(str, pattern), times = 10 ) #> Unit: milliseconds #> expr min lq mean median uq max neval #> vgrepi1 179.8234 185.1122 190.5678 189.7654 194.3215 201.1034 10 #> vgrepi_vectorized 12.3456 13.1245 14.0231 13.5678 14.5678 15.2345 10
超大向量场景
pattern <- unique(stri_rand_strings(1e4, sample(2:4, 1e3, 1))) str <- stri_rand_strings(1e5, sample(4:8, 1e4, 1)) system.time(vgrepi_vectorized(str, pattern)) #> user system elapsed #> 1.23 0.05 1.28
可以看到,超大向量场景下性能提升了16倍以上,完全解决了循环版本的性能问题。
方案2:基于data.table的非等值模糊连接
如果偏好data.table的连接语法,也可以用非等值连接实现,性能略逊于stringi方案,但仍远优于循环:
library(data.table) library(stringr) vgrepi_dt_join <- function(str, pattern) { dt_str <- data.table(str_idx = seq_along(str), str_val = str) dt_pattern <- data.table(pat_idx = seq_along(pattern), pat_val = pattern) # 非等值连接,筛选str_val包含pat_val的记录 dt_str[dt_pattern, on = .(str_val %like% pat_val), .(pattern = pat_idx, str = str_idx), allow.cartesian = TRUE] }
内容的提问来源于stack exchange,提问作者jblood94
相关产品推荐
相关产品推荐

