Regex 正则表达式的Kotlin性能问题

Regex 正则表达式的Kotlin性能问题,regex,performance,kotlin,jvm,performance-testing,Regex,Performance,Kotlin,Jvm,Performance Testing,我正在制作一个使用正则表达式的日志解析应用程序,我看到一些奇怪的行为,我希望有人能帮助解释,也许能给出一些克服这些行为的技巧。首先,代码如下: import java.io.File var regex1Count = 0 var regex2Count = 0 var noMatchCount = 0 val regex1 = Regex(".*error.*", RegexOption.IGNORE_CASE) val regex2 = Regex("exce

我正在制作一个使用正则表达式的日志解析应用程序,我看到一些奇怪的行为,我希望有人能帮助解释,也许能给出一些克服这些行为的技巧。首先,代码如下:

import java.io.File

var regex1Count = 0
var regex2Count = 0
var noMatchCount = 0
val regex1 = Regex(".*error.*", RegexOption.IGNORE_CASE)
val regex2 = Regex("exception|crashed|death|fatal|killed| f | e ", RegexOption.IGNORE_CASE)

fun main(args: Array<String>) {
    val file = File("C:\\Users\\pnogas\\Desktop\\mobicontrol.log")
    val time = System.currentTimeMillis()
    val result = file.useLines { sequence ->
        sequence.mapNotNull { line ->
            parseLine(line)
        }.toList()
    }
    println("took ${(System.currentTimeMillis() - time) / 1000.0} seconds")
    println("regex1Count = $regex1Count, regex2Count = $regex2Count, noMatchCount = $noMatchCount")
}

private fun parseLine(line: String) {
    for (filter in listOf(regex2, regex1)) {
        if (filter.containsMatchIn(line)) {
            if (regex1 == filter) {
                regex1Count++
            } else if (regex2 == filter) {
                regex2Count++
            }
            return
        }
    }
    noMatchCount++
}
但是如果我将一行更改为listOf(regex1,regex2),而不是listOf(regex2,regex1):

我知道通配符regex的运行成本会更高,但数字表明,更改顺序只会使其运行两倍以上,与处理的总行数相比,这似乎可以忽略不计。如果我使列表只包含regex1,则会获得相同的性能

最重要的是,当我使用记事本++对同一个文件执行相同的正则表达式搜索时,我得到了18个结果,但结果几乎是立即出现的。我知道JVM的性能不如本机编译代码,但它真的会运行得那么慢吗。还是我完全走错了路

由于到目前为止的答复,一些澄清

  • 我同意用“error”代替“.*error.*”可以解决这里的时间问题。对我来说,问题在于,用于生成正则表达式的字符串将来自应用程序中的用户输入。我想我可以在我的应用程序中做的一件事是一些预处理:(例如,删除任何前导或尾随的通配符,并保留内部通配符)

  • 我知道,因为第一个匹配返回的正则表达式,所以顺序很重要。同样,由于这将来自用户输入,我同意让他们承担选择性能订单的责任

  • 欢迎使用其他性能改进技巧,但我想在这里回答的主要问题是,为什么在我的示例中,顺序会产生如此大的差异?除非我错过了什么

  • 在第一个示例中: regex2运行11676次(匹配101次) regex1运行11575次(不运行匹配的101次regex2)

    在第二个例子中: regex1运行11676次(匹配18次) regex2运行11658次(未运行匹配的18次regex1)

    因此,regex1的运行次数增加了0.86%,regex2的运行次数减少了0.15%,但运行时间增加了754%?!
    我唯一的随机猜测是,有某种JIT JVM预热首先运行简单的正则表达式,这允许第二个更复杂的正则表达式运行得更快,我应该插入一个虚拟正则表达式,它在执行我关心的正则表达式以提高性能之前总是很快的…???

    这是一个复杂的问题,因此提前为长时间的问题道歉(遗憾的是不完整)答案

    您的测试代码存在误解。列表中的第一个正则表达式将在所有行上求值,因此在您的示例中求值11676次。您的regex1Count变量仅返回一个匹配项被返回的次数(代价高昂)搜索操作。因此,更改正则表达式的求值顺序会对性能产生巨大影响,因为第一个正则表达式将用作主过滤器

    此外,正如@PiRocks所说,正则表达式可以简化。更重要的是,由于它的简单性(搜索单个单词),这里甚至不需要使用正则表达式。您可以执行文字搜索,而且速度会快得多

    此外,作为多年的JVM用户,我必须纠正一个关于性能的常见误解:JVM应用程序并不总是比本地应用程序慢。每种技术都在其自己的领域中大放异彩,要获得最大性能,通常需要为正确的任务选择正确的工具。例如,JVM使用JIT对频率进行积极的优化垃圾收集器大大降低了可变分配的成本

    无论如何,在当前情况下,我们无法将手工代码的性能与附带的应用程序进行比较,无论双方使用何种技术。为什么?因为我们无法确定是否比较等效的工作流。在这里,记事本可能有:

    • 在内存中缓冲整个文件
    • 事先创建了索引
    • 分析了输入搜索正则表达式,并在执行前消除了不必要的复杂性
    • 多线程搜索
    • 等等
    我试图通过科特林游乐场重现你的案例:

  • 生成随机文本行
  • 测试您的
    regex1
  • 通过java模式api比较相同的正则表达式
  • 测试
    regex2
  • 对单词“error”执行文字搜索
  • 结果是显而易见的:与
    *错误相比,文字搜索速度非常快。*
    regex。regex是一个非常强大的工具,但它们的复杂性可能很难管理

    现在,还有一个问题:JVM正则表达式在性能方面是否实现得很差?要回答这个问题并不简单。天真地说,我们可以尝试一下我做的游戏(见下面的代码),用另一种语言重写它,并比较两者的输出。但由于JVM JIT/预热时间的原因,比较会有偏差

    我们必须对这两个实现进行广泛的循环,收集统计数据,最后比较结果,以获得良好的洞察力

    以下是游乐场及其输出,仅供参考:

    导入kotlin.random.random
    导入java.lang.StringBuilder
    导入java.lang.System
    导入java.util.regex.Pattern
    主要内容(){
    println(“日志示例:”)
    generateLogs(nbLines=2,wordPerLine=10).forEach{println(it)}
    println(“\n测量12000行上的正则表达式\n”)
    val regex1=Regex(“.*error.*”,RegexOption.IGNORE_CASE)
    for(列表(10,20)中的单词){
    粗略测量(“每行$nbWords的正则表达式1”){
    val matched=generateLogs(wordPerLine=nbWords)
    .count{regex1.containsMatchIn(it)}
    }
    }
    val javaPattern=Pattern.compile(“.*error.*”,Pattern.CASE\u不区分大小写)
    for(列表(10,20)中的单词){
    粗略测量(“每行$nbWords的Java模式1”){
    val=gen
    
    took 4.198 seconds
    regex1Count = 16, regex2Count = 101, noMatchCount = 11559
    
    took 35.049 seconds
    regex1Count = 18, regex2Count = 99, noMatchCount = 11559
    
    Log example :
    ex quam Suspendisse  vel sed  rhoncus aliquet. elit.
    nibh amet, sed  nibh eleifend diam amet ex eleifend.
    
    Measure Regex on 12000 lines
    
    Regex 1 for 10 words per line took 0.439 seconds
    Regex 1 for 20 words per line took 0.843 seconds
    Java pattern 1 for 10 words per line took 0.407 seconds
    Java pattern 1 for 20 words per line took 1.347 seconds
    Regex 2 for 50 words per line took 0.463 seconds
    Literal search for 1000 words per line took 0.836 seconds
    
    import kotlin.random.Random
    import java.lang.StringBuilder
    import java.lang.System
    import java.util.regex.Pattern
    
    fun main() {
        println("Log example :")
        generateLogs(nbLines = 2, wordPerLine = 10).forEach { println(it) }
        
        println("\nMeasure Regex on 12000 lines\n")
        
        val regex1 = Regex(".*error.*", RegexOption.IGNORE_CASE)
        for (nbWords in listOf(10, 20)) {
            roughMeasurement("Regex 1 for $nbWords words per line") {
                val matched = generateLogs(wordPerLine = nbWords)
                    .count { regex1.containsMatchIn(it) }
            }
        }
        
        val javaPattern = Pattern.compile(".*error.*", Pattern.CASE_INSENSITIVE)
        for (nbWords in listOf(10, 20)) {
            roughMeasurement("Java pattern 1 for $nbWords words per line") {
                val matched = generateLogs(wordPerLine = nbWords)
                    .count { javaPattern.matcher(it).find() }
            }
        }
        
        val regex2 = Regex("(exception)|(crashed)|(death)|(fatal)|(killed)| f | e ", RegexOption.IGNORE_CASE)
        roughMeasurement("Regex 2 for 50 words per line") {
            val matched = generateLogs()
                .count { regex2.containsMatchIn(it) }
        }
        
        roughMeasurement("Literal search for 1000 words per line") {
            val matched = generateLogs(wordPerLine = 1000)
                .count { it.indexOf("error") >= 0 }
        }
    }
    
    fun roughMeasurement(title: String, action: () -> Unit) {
        val start = System.nanoTime()
        action()
        val end = System.nanoTime()
        val timeSeconds = (end - start).toDouble() * 1e-9
        println("$title took ${"%.3f".format(timeSeconds)} seconds")
    }
    
    /* 
     * LOG GENERATION UTILITIES
     */
    
    
    fun generateLogs(nbLines : Int = 12000, wordPerLine : Int = 50) : Sequence<String> {
        return (1..nbLines).asSequence()
                           .map { generateSentence(wordPerLine) }   
    }
    
    fun generateSentence(nbWords : Int) : String {
        require(nbWords > 2) { "Need more than two words per sentence" }
        val builder = StringBuilder(nbWords * 3)
        for (i in 0..nbWords-2) {
            builder.append(wordPool.pick()).append(' ')
        }
        builder.append(wordPool.pick())
        
        return builder.toString()
    }
    
    fun List<String>.pick() = this[Random.nextInt(0, size)]
    
    /** 
     * Authorized words in log generation. 
     * To test for worst-case scenario, we've omitted searched keywords: 
     * error exception crashed death fatal killed
     */
    val wordPool = """
    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
    Suspendisse eu ex eu ligula egestas posuere ac et velit.
    Fusce sed nisl diam. Proin eleifend nibh vel felis fermentum,
    a luctus diam eleifend. Pellentesque feugiat magna sit amet 
    arcu eleifend, vel lacinia justo aliquet. In quam magna, 
    rhoncus a lacinia vel.
    """.split(Regex("\\s+"))