正则表达式对小字符串的强力攻击

时间:2016-10-23 18:26:33

标签: regex string performance brute-force

当测试小字符串(例如isPhoneNumber或isHexadecimal)时,使用正则表达式会带来性能上的好处,还是会强制它们更快?通过检查给定字符串的字符是否在指定范围内比使用正则表达式更快,不会强制它们吗?

例如:

public static boolean isHexadecimal(String value)
{
    if (value.startsWith("-"))
    {
        value = value.substring(1);
    }

    value = value.toLowerCase();

    if (value.length() <= 2 || !value.startsWith("0x"))
    {
        return false;
    }

    for (int i = 2; i < value.length(); i++)
    {
        char c = value.charAt(i);

        if (!(c >= '0' && c <= '9' || c >= 'a' && c <= 'f'))
        {
            return false;
        }
    }

    return true;
}

VS

Regex.match(/0x[0-9a-f]+/, "0x123fa") // returns true if regex matches whole given expression

似乎有一些与正则表达式相关的开销,即使模式是预编译的,只是因为正则表达式必须在许多一般情况下工作。相比之下,蛮力方法完全符合要求而不再需要。我错过了正则表达式的一些优化吗?

8 个答案:

答案 0 :(得分:8)

检查字符串字符是否在特定范围内正是构建正则表达式的原因。他们将表达式转换为原子系列指令;他们基本上是在较低的层次上写出你的手动解析步骤。

正则表达式往往缓慢的是将表达式转换为指令。当多次使用正则表达式时,您可以看到真正的性能提升。那时你可以提前编译表达式,然后在匹配,搜索,替换等中简单地应用生成的编译指令。

与性能有关的情况一样,执行一些测试并测量结果

答案 1 :(得分:6)

我写了一个小基准来估算:

的表现
  • NOP方法(了解基线迭代速度);
  • 原始方法,由OP提供;
  • 正则表达式;
  • Compiled Regexp;
  • @maraca提供的版本(没有 toLowerCase substring );
  • “fastIsHex”版本(基于开关),我添加的只是为了好玩。

测试机配置如下:

  • JVM:Java(TM)SE运行时环境(版本1.8.0_101-b13)
  • CPU:Intel(R)Core(TM)i5-2500 CPU @ 3.30GHz

以下是我为原始测试字符串“0x123fa”和10.000.000次迭代获得的结果:

Method "NOP" => #10000000 iterations in 9ms
Method "isHexadecimal (OP)" => #10000000 iterations in 300ms
Method "RegExp" => #10000000 iterations in 4270ms
Method "RegExp (Compiled)" => #10000000 iterations in 1025ms
Method "isHexadecimal (maraca)" => #10000000 iterations in 135ms
Method "fastIsHex" => #10000000 iterations in 107ms

正如您所看到的,OP的原始方法比RegExp方法更快(至少在使用JDK提供的RegExp实现时)。

(供你参考)

基准代码:

public static void main(String[] argv) throws Exception {
    //Number of ITERATIONS
    final int ITERATIONS = 10000000;

    //NOP
    benchmark(ITERATIONS,"NOP",() -> nop(longHexText));

    //isHexadecimal
    benchmark(ITERATIONS,"isHexadecimal (OP)",() -> isHexadecimal(longHexText));

    //Un-compiled regexp
    benchmark(ITERATIONS,"RegExp",() -> longHexText.matches("0x[0-9a-fA-F]+"));

    //Pre-compiled regexp
    final Pattern pattern = Pattern.compile("0x[0-9a-fA-F]+");
    benchmark(ITERATIONS,"RegExp (Compiled)", () -> {
        pattern.matcher(longHexText).matches();
    });

    //isHexadecimal (maraca)
    benchmark(ITERATIONS,"isHexadecimal (maraca)",() -> isHexadecimalMaraca(longHexText));

    //FastIsHex
    benchmark(ITERATIONS,"fastIsHex",() -> fastIsHex(longHexText));
}

public static void benchmark(int iterations,String name,Runnable block) {
    //Start Time
    long stime = System.currentTimeMillis();

    //Benchmark
    for(int i = 0; i < iterations; i++) {
        block.run();
    }

    //Done
    System.out.println(
        String.format("Method \"%s\" => #%d iterations in %dms",name,iterations,(System.currentTimeMillis()-stime))
    );
}

NOP方法:

public static boolean nop(String value) { return true; }

fastIsHex方法:

public static boolean fastIsHex(String value) {

    //Value must be at least 4 characters long (0x00)
    if(value.length() < 4) {
        return false;
    }

    //Compute where the data starts
    int start = ((value.charAt(0) == '-') ? 1 : 0) + 2;

    //Check prefix
    if(value.charAt(start-2) != '0' || value.charAt(start-1) != 'x') {
        return false;
    }

    //Verify data
    for(int i = start; i < value.length(); i++) {
        switch(value.charAt(i)) {
            case '0':case '1':case '2':case '3':case '4':case '5':case '6':case '7':case '8':case '9':
            case 'a':case 'b':case 'c':case 'd':case 'e':case 'f':
            case 'A':case 'B':case 'C':case 'D':case 'E':case 'F':
                continue;

            default:
                return false;
        }
    }

    return true;
}

所以,答案是否定的,对于短字符串和手头的任务,RegExp并不快。

当谈到更长的琴弦时,余额是完全不同的, 下面是8192长十六进制字符串的结果,我用:

生成
hexdump -n 8196 -v -e '/1 "%02X"' /dev/urandom

和10.000次迭代:

Method "NOP" => #10000 iterations in 2ms
Method "isHexadecimal (OP)" => #10000 iterations in 1512ms
Method "RegExp" => #10000 iterations in 1303ms
Method "RegExp (Compiled)" => #10000 iterations in 1263ms
Method "isHexadecimal (maraca)" => #10000 iterations in 553ms
Method "fastIsHex" => #10000 iterations in 530ms

正如您所看到的,手写方法( macara 和我的 fastIsHex )仍然击败了RegExp,但原始方法却没有, (由于 substring() toLowerCase())。

旁注:

这个基准测试非常简单,只测试“最坏情况”场景(即一个完全有效的字符串),实际结果,混合数据长度和非0有效无效比率,可能会大不相同

更新

我还尝试了char []数组版本:

 char[] chars = value.toCharArray();
 for (idx += 2; idx < chars.length; idx++) { ... }

它甚至比 getCharAt(i)版本慢一点:

  Method "isHexadecimal (maraca) char[] array version" => #10000000 iterations in 194ms
  Method "fastIsHex, char[] array version" => #10000000 iterations in 164ms

我猜是因为 toCharArray 中的数组副本。

更新(#2):

我已经运行了额外的8k / 100.000迭代测试,看看“maraca”和“fastIsHex”方法之间的速度是否存在任何真正的差异,并且还将它们标准化为使用完全相同的前置条件代码:

运行#1

Method "isHexadecimal (maraca) *normalized" => #100000 iterations in 5341ms
Method "fastIsHex" => #100000 iterations in 5313ms

运行#2

Method "isHexadecimal (maraca) *normalized" => #100000 iterations in 5313ms
Method "fastIsHex" => #100000 iterations in 5334ms

即。这两种方法之间的速度差异最多是微不足道的,可能是由于测量错误(因为我在我的工作站上运行它而不是专门设置的清洁测试环境)。

答案 2 :(得分:4)

解决问题的蛮力方法是系统地测试所有组合。这不是你的情况。

可以通过手写程序获得更好的性能。如果您事先知道,可以利用数据分发。或者您可以制作适用于您的案例的一些聪明的快捷方式。但实际上并不能保证你所写的内容会自动快于正则表达式。正则表达式的实现也得到了优化,你可以很容易地得到比这更差的代码。

你问题中的代码真的没什么特别的,很可能它与正则表达式相同。当我测试它时,没有明显的赢家,有时一个更快,有时另一个,差异很小。你的时间有限,明智地考虑你花在哪里。

答案 3 :(得分:4)

你滥用了“蛮力”一词。&#34;一个更好的术语是 ad hoc 自定义匹配。

正则表达式解释器通常比自定义模式匹配器慢。正则表达式被编译成字节代码,编译需要时间。甚至忽略编译(如果你只编译一次并且匹配一个很长的字符串和/或很多次编译成本并不重要)可能会很好,在匹配的解释器中花费的机器指令是自定义匹配器没有的开销。 #39; t。

在正则表达式匹配器胜出的情况下,正常情况下正则表达式引擎是以非常快的本机代码实现的,而自定义匹配器是用较慢的东西编写的。

现在你可以将正则表达式编译为本机代码,其运行速度与完成的自定义匹配器一样快。这是例如lex / flex等。但是最常见的库或内置语言不采用这种方法(Java,Python,Perl等)。他们使用口译员。

本机代码生成库往往使用起来很麻烦,除非在C / C ++中,它们已经成为空气中的一部分已经存在了几十年。

在其他语言中,我是国家机器的粉丝。对我来说,它们比正则表达式或自定义匹配器更容易理解和正确。以下是您的问题。状态0是开始状态,D代表十六进制数字。

state machines

机器的实施可以非常快。在Java中,它可能如下所示:

static boolean isHex(String s) {
  int state = 0;
  for (int i = 0; i < s.length(); i++) {
    char c = s.charAt(i);
    switch (state) {
      case 0:
        if (c == '-') state = 1;
        else if (c == '0') state = 2;
        else return false;
        break;
      case 1:
        if (c == '0') state = 2;
        else return false;
        break;
      case 2:
        if (c == 'x') state = 3;
        else return false;
        break;
      case 3:
        if (isHexDigit(c)) state = 4;
        else return false;
        break;
      case 4:
        if (isHexDigit(c)) ; // state already = 4
        else return false;
        break;
    }
  }
  return true;
}

static boolean isHexDigit(char c) {
  return '0' <= c && c <= '9' || 'A' <= c && c <= 'F' || 'a' <= c && c <= 'f';
}

代码不是超短,但它是图表的直接翻译。没有什么可以搞砸简单的印刷错误。

在C中,您可以将状态实施为goto标签:

int isHex(char *s) {
  char c;
  s0:
    c = *s++;
    if (c == '-') goto s1;
    if (c == '0') goto s2;
    return 0;
  s1:
    c = *s++;
    if (c == '0') goto s2;
    return 0;
  s2:
    c = *s++;
    if (c == 'x') goto s3;
    return 0;
  s3:
    c = *s++;
    if (isxdigit(c)) goto s4;
    return 0;
  s4: 
    c = *s++;
    if (isxdigit(c)) goto s4;
    if (c == '\0') return 1;
    return 0;
}

这种用C语言编写的goto匹配器通常是我见过的最快的。在我的MacBook上使用旧的gcc(4.6.4),这个只能编译35个机器指令。

答案 4 :(得分:1)

通常情况更好取决于您的目标。如果可读性是主要目标(应该是什么,除非您检测到性能问题),那么正则表达式就好了。

如果性能是您的目标,那么您必须首先分析问题。例如。如果你知道它是电话号码或十六进制数字(没有别的)那么问题会变得更加简单。

现在让我们看看你的函数(性能方面)来检测十六进制数字:

  1. 获取子字符串很糟糕(通常会创建一个新对象),更好地使用索引并推进它。
  2. 不是使用toLower(),而是与大写和小写字母进行比较(字符串只迭代一次,不会执行多余的替换,也不会创建新对象)。
  3. 因此,性能优化的版本可能看起来像这样(您可以使用charArray而不是字符串进一步优化):

    public static final boolean isHexadecimal(String value) {
      if (value.length() < 3)
        return false;
      int idx;
      if (value.charAt(0) == '-' || value.charAt(0) == '+') { // also supports unary plus
        if (value.length() < 4) // necessairy because -0x and +0x are not valid
          return false;
        idx = 1;
      } else {
        idx = 0;
      }
      if (value.chartAt(idx) != '0' || value.charAt(idx + 1) != 'x')
        return false;
      for (idx += 2; idx < value.length(); idx++) {
        char c = value.charAt(idx);
        if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')))
          return false;
      }
      return true;
    }
    

答案 5 :(得分:0)

Well implemented正则表达式可以比相同模式的天真暴力执行更快。 另一方面,您始终可以针对特定案例实施更快的解决方案。 同样如上文所述,流行语言中的大多数实现都不高效(在某些情况下)。

只有在性能是绝对优先级并且进行大量测试和分析时,我才会实现自己的解决方案。

答案 6 :(得分:0)

为了获得比天真的手写编码验证器更好的性能,您可以使用基于确定性自动机的正则表达式库,例如: Brics Automaton

我写了一个简短的jmh基准:

@State(Scope.Thread)
public abstract class MatcherBenchmark {

   private String longHexText;

   @Setup
   public void setup() {
     initPattern("0x[0-9a-fA-F]+");
     this.longHexText = "0x123fa";
   }

   public abstract void initPattern(String pattern);

   @Benchmark
   @BenchmarkMode(Mode.AverageTime)
   @OutputTimeUnit(TimeUnit.MICROSECONDS)
   @Warmup(iterations = 10)
   @Measurement(iterations = 10)
   @Fork(1)
   public void benchmark() {
     boolean result =  benchmark(longHexText);
     if (!result) {
        throw new RuntimeException();
     }
   }

   public abstract boolean benchmark(String text);

   @TearDown
   public void tearDown() {
     donePattern();
     this.longHexText = null;
   }

   public abstract void donePattern();

}

并通过以下方式实施:

@Override
public void initPattern(String pattern) {
    RegExp r = new RegExp(pattern);
    this.automaton = new RunAutomaton(r.toAutomaton(true));
}

@Override
public boolean benchmark(String text) {
    return automaton.run(text);
}

我还为Zeppelins,Genes和已编译的java.util.Regex解决方案以及rexlex的解决方案创建了基准。这些是我机器上jmh基准测试的结果:

BricsMatcherBenchmark.benchmark      avgt   10  0,014 �  0,001  us/op
GenesMatcherBenchmark.benchmark      avgt   10  0,017 �  0,001  us/op
JavaRegexMatcherBenchmark.benchmark  avgt   10  0,097 �  0,005  us/op
RexlexMatcherBenchmark.benchmark     avgt   10  0,061 �  0,002  us/op
ZeppelinsBenchmark.benchmark         avgt   10  0,008 �  0,001  us/op

使用非十六进制数0x123fax启动相同的基准测试会产生以下结果(注意:我在benchmark中反转了此基准的验证)

BricsMatcherBenchmark.benchmark      avgt   10  0,015 �  0,001  us/op
GenesMatcherBenchmark.benchmark      avgt   10  0,019 �  0,001  us/op
JavaRegexMatcherBenchmark.benchmark  avgt   10  0,102 �  0,001  us/op
RexlexMatcherBenchmark.benchmark     avgt   10  0,052 �  0,002  us/op
ZeppelinsBenchmark.benchmark         avgt   10  0,009 �  0,001  us/op

答案 7 :(得分:-2)

正则表达式有很多优点,但Regex确实存在性能问题。