Python
Go
正規表現
golang
sip

Golangの正規表現がどれぐらいおそいのかをPythonと比較してみた話

Screenshot from 2018-04-14 16-55-04.png

最近Packetbeatのプラグインを作っている中でGolangを書いてますが、Golangといえば正規表現が遅いとよく聞きますがほんとに遅いのかを試してみました。

今回はみんな大好きPythonと比較。

きっと言語それぞれに得意なケース、得意でないケースがあるかとは思いますが、
あくまでも実用コードの比較ということで・・・

入出力

SIPのname-addrをパースする正規表現の実行速度を比較したいと思いいます。

"Display-name"<sip:0312341234@10.0.0.1:5060>;tag=tag12345678といった入力の時に以下のように分解する正規表現を試します。

  • Display-name
  • sip
  • 0312341234
  • 10.0.0.1
  • 5060
  • tag=tag12345678

Golangのコード

package main

import (
    "fmt"
    "regexp"
)

func parseSIPAddrRegexp(addr string) {
    rep := regexp.MustCompile(`^["]{0,1}([^"]*)["]{0,1}[ ]*<(sip|tel|sips):(([^@]*)@){0,1}([^>^:]*|\[[a-fA-F0-9:]*\]):{0,1}([0-9]*){0,1}>(;.*){0,1}$`)
    result := rep.FindSubmatch([]byte(addr))
    fmt.Printf("%s\n",result)
}

func main(){
    name_addrs:=[]string{"\"display_name\"<sip:0312341234@10.0.0.1:5060>;user=phone;hogehoge",
                         "<sip:0312341234@10.0.0.1>",
                         "\"display_name\"<sip:0312341234@10.0.0.1>",
                         "<sip:whois.this>;user=phone",
                         "\"0333334444\"<sip:[2001:30:fe::4:123]>;user=phone"}
    for i:=0; i< 100000 ; i ++{
            for idx:= range name_addrs{
                parseSIPAddrRegexp(name_addrs[idx])
        }
    }
}

繰り返し回数が1回の場合の出力結果は以下の通り

["display_name"<sip:0312341234@10.0.0.1:5060>;user=phone;hogehoge display_name sip 0312341234@ 0312341234 10.0.0.1 5060 ;user=phone;hogehoge]
[<sip:0312341234@10.0.0.1>  sip 0312341234@ 0312341234 10.0.0.1  ]
["display_name"<sip:0312341234@10.0.0.1> display_name sip 0312341234@ 0312341234 10.0.0.1  ]
[<sip:whois.this>;user=phone  sip   whois.this  ;user=phone]
["0333334444"<sip:[2001:30:fe::4:123]>;user=phone 0333334444 sip   [2001:30:fe::4:123]  ;user=phone]

Pythonのコード

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import re

def parseSIPAddrRegexp(addr):
    rep = re.compile(r'^["]{0,1}([^"]*)["]{0,1}[ ]*<(sip|tel|sips):(([^@]*)@){0,1}([^>^:]*|\[[a-fA-F0-9:]*\]):{0,1}([0-9]*){0,1}>(;.*){0,1}$')
    result=rep.match(addr)
    print(result.groups())

if __name__ == "__main__":
    name_addrs=["\"display_name\"<sip:0312341234@10.0.0.1:5060>;user=phone;hogehoge",
                "<sip:0312341234@10.0.0.1>",
                "\"display_name\"<sip:0312341234@10.0.0.1>",
                "<sip:whois.this>;user=phone",
                "\"0333334444\"<sip:[2001:30:fe::4:123]>;user=phone"]

    for i in range(100000):
        for idx in range(len(name_addrs)):
            parseSIPAddrRegexp(name_addrs[idx])

繰り返し回数が1回の場合の出力結果は以下の通り

('display_name', 'sip', '0312341234@', '0312341234', '10.0.0.1', '5060', ';user=phone;hogehoge')
('', 'sip', '0312341234@', '0312341234', '10.0.0.1', '', None)
('display_name', 'sip', '0312341234@', '0312341234', '10.0.0.1', '', None)
('', 'sip', None, None, 'whois.this', '', ';user=phone')

測定

上記コードはともに100,000回同じ処理を繰り替えすように組んでいるので、実行時にtimeコマンドを使いcpu totalを計測したいと思います。計測は5回行い、その最大値、中央値、平均値、最小値を比較します。
また、標準出力はすべて/dev/nullにすべて捨てるようにします。

Golangの実行結果

$ for i in `seq 5`;do time go run regexp_sip.go >/dev/null ;done  
go run regexp_sip.go > /dev/null  27.06s user 1.15s system 111% cpu 25.359 total
go run regexp_sip.go > /dev/null  27.18s user 1.01s system 110% cpu 25.497 total
go run regexp_sip.go > /dev/null  27.33s user 1.24s system 110% cpu 25.943 total
go run regexp_sip.go > /dev/null  26.90s user 1.12s system 110% cpu 25.251 total
go run regexp_sip.go > /dev/null  27.26s user 1.12s system 110% cpu 25.793 total

Pythonの実行結果

$ for i in `seq 5`;do time python3 regexp_sip.py >/dev/null ;done
python3 regexp_sip.py > /dev/null  3.78s user 0.02s system 99% cpu 3.798 total
python3 regexp_sip.py > /dev/null  3.44s user 0.00s system 99% cpu 3.448 total
python3 regexp_sip.py > /dev/null  3.85s user 0.00s system 99% cpu 3.856 total
python3 regexp_sip.py > /dev/null  3.54s user 0.02s system 99% cpu 3.561 total
python3 regexp_sip.py > /dev/null  3.48s user 0.00s system 99% cpu 3.481 total

比較結果

まさかこんなに違うとは正直思っていなかったです・・・

Golang Python
最大値 25.943 3.856
中央値 25.497 3.561
平均値 25.5686 3.6288
最小値 25.251 3.448

[2018/04/14 21:20追記]

どちらかというと、「正規表現のコンパイル速度」をはかっているように見えますが・・・

というコメントをいただき、なるほど、たしかにそういうことか・・・ということで再度検証しました。

[23:11追記]

コンパイルつどつどしてるのを突っ込まれそうでしたのでビルド済みで測定しなおしておきます。

コンパイル済みの正規表現を渡すように改変しています。

Go版

package main

import (
    "fmt"
    "regexp"
)

func parseSIPAddrRegexp(addr string, rep *regexp.Regexp) {
    result := rep.FindSubmatch([]byte(addr))
    fmt.Printf("%s\n",result)
}

func main(){
    name_addrs:=[]string{"\"display_name\"<sip:0312341234@10.0.0.1:5060>;user=phone;hogehoge",
                         "<sip:0312341234@10.0.0.1>",
                         "\"display_name\"<sip:0312341234@10.0.0.1>",
                         "<sip:whois.this>;user=phone",
                         "\"0333334444\"<sip:[2001:30:fe::4:123]>;user=phone"}

    rep := regexp.MustCompile(`^["]{0,1}([^"]*)["]{0,1}[ ]*<(sip|tel|sips):(([^@]*)@){0,1}([^>^:]*|\[[a-fA-F0-9:]*\]):{0,1}([0-9]*){0,1}>(;.*){0,1}$`)
    for i:=0; i< 100000 ; i ++{
            for idx:= range name_addrs{
                parseSIPAddrRegexp(name_addrs[idx],rep)
        }
    }

実行結果

$ for i in `seq 5`;do time ./regexp_sip >/dev/null ;done      
./regexp_sip > /dev/null  4.44s user 0.15s system 100% cpu 4.563 total
./regexp_sip > /dev/null  4.42s user 0.20s system 100% cpu 4.588 total
./regexp_sip > /dev/null  4.50s user 0.14s system 100% cpu 4.613 total
./regexp_sip > /dev/null  4.47s user 0.16s system 100% cpu 4.611 total
./regexp_sip > /dev/null  4.46s user 0.20s system 100% cpu 4.640 total

Python版

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import re

def parseSIPAddrRegexp(addr,rep):
    result=rep.match(addr)
    print(result.groups())

if __name__ == "__main__":
    name_addrs=["\"display_name\"<sip:0312341234@10.0.0.1:5060>;user=phone;hogehoge",
                "<sip:0312341234@10.0.0.1>",
                "\"display_name\"<sip:0312341234@10.0.0.1>",
                "<sip:whois.this>;user=phone",
                "\"0333334444\"<sip:[2001:30:fe::4:123]>;user=phone"]

    rep = re.compile(r'^["]{0,1}([^"]*)["]{0,1}[ ]*<(sip|tel|sips):(([^@]*)@){0,1}([^>^:]*|\[[a-fA-F0-9:]*\]):{0,1}([0-9]*){0,1}>(;.*){0,1}$')
    for i in range(100000):
        for idx in range(len(name_addrs)):
            parseSIPAddrRegexp(name_addrs[idx],rep)

実行結果

$ for i in `seq 5`;do time python3 regexp_sip.py >/dev/null ;done
python3 regexp_sip.py > /dev/null  3.02s user 0.00s system 99% cpu 3.039 total
python3 regexp_sip.py > /dev/null  2.92s user 0.00s system 99% cpu 2.928 total
python3 regexp_sip.py > /dev/null  2.99s user 0.01s system 99% cpu 3.014 total
python3 regexp_sip.py > /dev/null  2.96s user 0.01s system 99% cpu 2.983 total
python3 regexp_sip.py > /dev/null  3.09s user 0.01s system 99% cpu 3.106 total

比較

Golang Python
最大値 4.64 3.106
中央値 4.611 3.014
平均値 4.603 3.014
最小値 4.563 2.928

ぐっと縮まりましたが、まだそれでもGolangのほうが遅いですね。

Pythonはいちいちコンパイルしなくても速いというのをどこかで読んだことありますが、ほか言語にそれを当てはめてコーディングするとダメな例ですね。ご指摘ありがとうございました!