何不 Ack?Grep, Ack, Ag的搜索效率對比

jopen 9年前發布 | 17K 次閱讀 Linux grep

前言

我(@董偉明9 )經常看到很多程序員, 運維在代碼搜索上使用ack, 甚至ag(the_silver_searcher ), 而我工作中95%都是用grep,剩下的是ag。 我覺得很有必要聊一聊這個話題。

我以前也是一個運維, 我當時也希望找到最好的最快的工具用在工作的方方面面。 但是我很好奇為什么ag和ack沒有作為linux發行版的內置部分。 內置的一直是grep。 我當初的理解是受各種開源協議的限制, 或者發行版的boss個人喜好。 后來我就做了實驗, 研究了下他們到底誰快。 當時的做法也無非跑幾個真實地線上log看看用時。 然后我也有了我的一個認識: 大部分時候用grep也無妨, 日志很大的時候用ag。

ack原來的域名是betterthangrep.com, 現在是beyondgrep.com。 好吧,其實我理解使用ack的同學, 也理解ack產生的原因。 這里就有個故事。

何不 Ack?Grep, Ack, Ag的搜索效率對比

最開始我做運維使用shell, 經常做一些分析日志的工作。 那時候經常寫比較復雜的shell代碼實現一些特定的需求。 后來來了一位會perl的同學。 原來我寫shell做一個事情, 寫了20多行shell代碼, 跑一次大概5分鐘, 這位同學來了用perl改寫, 4行, 一分鐘就能跑完。 亮瞎我們的眼, 從那時候開始, 我就覺得需要學perl,以至于后來的python。

perl是天生用來文本解析的語言, ack的效率確實很高。 我想著可能是大家認為ack要更快更合適的理由吧。 其實這件事要看場景。 我為什么還用比較’土’的grep呢?

看一下這篇文章, 希望給大家點啟示。不耐煩看具體測試過程的同學,可以直接看結論:

  1. 在搜索的總數據量較小的情況下, 使用grep, ack甚至ag在感官上區別不大
  2. 搜索的總數據量較大時, grep效率下滑的很多, 完全不要選
  3. ack在某些場景下沒有grep效果高(比如使用-v搜索中文的時候)
  4. 在不使用ag沒有實現的選項功能的前提下, ag完全可以替代ack/grep

實驗條件

PS: 嚴重聲明, 本實驗經個人實踐, 我盡量做到合理。 大家看完覺得有異議可以試著其他的角度來做。 并和我討論。

  • 我使用了公司的一臺開發機(gentoo)

  • 我測試了純英文和漢語2種, 漢語使用了結巴分詞的字典, 英語使用了miscfiles中提供的詞典

# 假如你是ubuntu: sudo apt-get install miscfiles
wget https://raw.githubusercontent.com/fxsjy/jieba/master/extra_dict/dict.txt.big

實驗前的準備

我會分成英語和漢語2種文件, 文件大小為1MB, 10MB, 100MB, 500MB, 1GB, 5GB。 沒有更多是我覺得在實際業務里面不會單個日志文件過大的。 也就沒有必要測試了(就算有, 可以看下面結果的趨勢)。用下列程序深入測試的文件:

cat make_words.py
# coding=utf-8

import os
import random
from cStringIO import StringIO

EN_WORD_FILE = '/usr/share/dict/words'
CN_WORD_FILE = 'dict.txt.big'
with open(EN_WORD_FILE) as f:
    EN_DATA = f.readlines()
with open(CN_WORD_FILE) as f:
    CN_DATA = f.readlines()
MB = pow(1024, 2)
SIZE_LIST = [1, 10, 100, 500, 1024, 1024 * 5]
EN_RESULT_FORMAT = 'text_{0}_en_MB.txt'
CN_RESULT_FORMAT = 'text_{0}_cn_MB.txt'


def write_data(f, size, data, cn=False):
    total_size = 0
    while 1:
        s = StringIO()
        for x in range(10000):
            cho = random.choice(data)
            cho = cho.split()[0] if cn else cho.strip()
            s.write(cho)
        s.seek(0, os.SEEK_END)
        total_size += s.tell()
        contents = s.getvalue()
        f.write(contents + '\n')
        if total_size > size:
            break
    f.close()


for index, size in enumerate([
        MB,
        MB * 10,
        MB * 100,
        MB * 500,
        MB * 1024,
        MB * 1024 * 5]):
    size_name = SIZE_LIST[index]
    en_f = open(EN_RESULT_FORMAT.format(size_name), 'a+')
    cn_f = open(CN_RESULT_FORMAT.format(size_name), 'a+')
    write_data(en_f, size, EN_DATA)
    write_data(cn_f, size, CN_DATA, True)

好吧, 效率比較低是吧? 我自己沒有vps, 公司服務器我不能沒事把全部內核的cpu都占滿(不是運維好幾年了)。 假如你不介意htop的多核cpu飄紅, 可以這樣,耗時就是各文件生成的時間短板。這是生成測試文件的多進程版本:

# coding=utf-8

import os
import random
import multiprocessing
from cStringIO import StringIO

EN_WORD_FILE = '/usr/share/dict/words'
CN_WORD_FILE = 'dict.txt.big'
with open(EN_WORD_FILE) as f:
    EN_DATA = f.readlines()
with open(CN_WORD_FILE) as f:
    CN_DATA = f.readlines()
MB = pow(1024, 2)
SIZE_LIST = [1, 10, 100, 500, 1024, 1024 * 5]
EN_RESULT_FORMAT = 'text_{0}_en_MB.txt'
CN_RESULT_FORMAT = 'text_{0}_cn_MB.txt'

inputs = []

def map_func(args):
    def write_data(f, size, data, cn=False):
        f = open(f, 'a+')
        total_size = 0
        while 1:
            s = StringIO()
            for x in range(10000):
                cho = random.choice(data)
                cho = cho.split()[0] if cn else cho.strip()
                s.write(cho)
            s.seek(0, os.SEEK_END)
            total_size += s.tell()
            contents = s.getvalue()
            f.write(contents + '\n')
            if total_size > size:
                break
        f.close()

    _f, size, data, cn = args
    write_data(_f, size, data, cn)


for index, size in enumerate([
        MB,
        MB * 10,
        MB * 100,
        MB * 500,
        MB * 1024,
        MB * 1024 * 5]):
    size_name = SIZE_LIST[index]
    inputs.append((EN_RESULT_FORMAT.format(size_name), size, EN_DATA, False))
    inputs.append((CN_RESULT_FORMAT.format(size_name), size, CN_DATA, True))

pool = multiprocessing.Pool()
pool.map(map_func, inputs, chunksize=1)

等待一段時間后,測試的文件生成了。目錄下是這樣的:

$ls -lh
total 14G
-rw-rw-r-- 1 vagrant vagrant 2.2K Mar 14 05:25 benchmarks.ipynb
-rw-rw-r-- 1 vagrant vagrant 8.2M Mar 12 15:43 dict.txt.big
-rw-rw-r-- 1 vagrant vagrant 1.2K Mar 12 15:46 make_words.py
-rw-rw-r-- 1 vagrant vagrant 101M Mar 12 15:47 text_100_cn_MB.txt
-rw-rw-r-- 1 vagrant vagrant 101M Mar 12 15:47 text_100_en_MB.txt
-rw-rw-r-- 1 vagrant vagrant 1.1G Mar 12 15:54 text_1024_cn_MB.txt
-rw-rw-r-- 1 vagrant vagrant 1.1G Mar 12 15:51 text_1024_en_MB.txt
-rw-rw-r-- 1 vagrant vagrant  11M Mar 12 15:47 text_10_cn_MB.txt
-rw-rw-r-- 1 vagrant vagrant  11M Mar 12 15:47 text_10_en_MB.txt
-rw-rw-r-- 1 vagrant vagrant 1.1M Mar 12 15:47 text_1_cn_MB.txt
-rw-rw-r-- 1 vagrant vagrant 1.1M Mar 12 15:47 text_1_en_MB.txt
-rw-rw-r-- 1 vagrant vagrant 501M Mar 12 15:49 text_500_cn_MB.txt
-rw-rw-r-- 1 vagrant vagrant 501M Mar 12 15:48 text_500_en_MB.txt
-rw-rw-r-- 1 vagrant vagrant 5.1G Mar 12 16:16 text_5120_cn_MB.txt
-rw-rw-r-- 1 vagrant vagrant 5.1G Mar 12 16:04 text_5120_en_MB.txt

確認版本

$  ack --version # ack在ubuntu下叫`ack-grep`
ack 2.12
Running under Perl 5.16.3 at /usr/bin/perl

Copyright 2005-2013 Andy Lester.

This program is free software.  You may modify or distribute it
under the terms of the Artistic License v2.0.
$  ag --version
ag version 0.21.0
$  grep --version
grep (GNU grep) 2.14
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by Mike Haertel and others, see <http://git.sv.gnu.org/cgit/grep.git/tree/AUTHORS>.

實驗設計

為了不產生并行執行的相互響應, 我還是選擇了效率很差的同步執行, 我使用了ipython提供的%timeit。 測試程序的代碼如下:

import re
import glob
import subprocess
import cPickle as pickle
from collections import defaultdict

IMAP = {
    'cn': ('豆瓣', '小明明'),
    'en': ('four', 'python')
}
OPTIONS = ('', '-i', '-v')
FILES = glob.glob('text_*_MB.txt')
EN_RES = defaultdict(dict)
CN_RES = defaultdict(dict)
RES = {
        'en': EN_RES,
        'cn': CN_RES
}
REGEX = re.compile(r'text_(\d+)_(\w+)_MB.txt')
CALL_STR = '{command} {option} {word} {filename} > /dev/null 2>&1'

for filename in FILES:
    size, xn = REGEX.search(filename).groups()
    for word in IMAP[xn]:
        _r = defaultdict(dict)
        for command in ['grep', 'ack', 'ag']:
            for option in OPTIONS:
                rs = %timeit -o -n10 subprocess.call(CALL_STR.format(command=command, option=option, word=word, filename=filename), shell=True)
                best = rs.best
                _r[command][option] = best
        RES[xn][word][size] = _r

# 存起來

data = pickle.dumps(RES)

with open('result.db', 'w') as f:
    f.write(data)

溫馨提示, 這是一個灰常耗時的測試。 開始執行后 要喝很久的茶…

我來秦皇島辦事完畢(耗時超過1一天), 繼續我們的實驗。

我想要的效果

我想工作的時候一般都是用到不帶參數/帶-i(忽略大小寫)/-v(查找不匹配項)這三種。 所以這里測試了:

  1. 英文搜索/中文搜索
  2. 選擇了2個搜索詞(效率太低, 否則可能選擇多個)
  3. 分別測試’’/’-i’/’-v’三種參數的執行
  4. 使用%timeit, 每種條件執行10遍, 選擇效率最好的一次的結果
  5. 每個圖代碼一個搜索詞, 3搜索命令, 一個選項在搜索不同大小文件時的效率對比

我先說結論

  1. 在搜索的總數據量較小的情況下, 使用grep, ack甚至ag在感官上區別不大
  2. 搜索的總數據量較大時, grep效率下滑的很多, 完全不要選
  3. ack在某些場景下沒有grep效果高(比如使用-v搜索中文的時候)
  4. 在不使用ag沒有實現的選項功能的前提下, ag完全可以替代ack/grep

渲染圖片的gist可以看這里benchmarks.ipynb。 它的數據來自上面跑的結果在序列化之后存入的文件。

何不 Ack?Grep, Ack, Ag的搜索效率對比 何不 Ack?Grep, Ack, Ag的搜索效率對比 何不 Ack?Grep, Ack, Ag的搜索效率對比 何不 Ack?Grep, Ack, Ag的搜索效率對比 何不 Ack?Grep, Ack, Ag的搜索效率對比 何不 Ack?Grep, Ack, Ag的搜索效率對比 何不 Ack?Grep, Ack, Ag的搜索效率對比  

何不 Ack?Grep, Ack, Ag的搜索效率對比 何不 Ack?Grep, Ack, Ag的搜索效率對比 何不 Ack?Grep, Ack, Ag的搜索效率對比 何不 Ack?Grep, Ack, Ag的搜索效率對比 何不 Ack?Grep, Ack, Ag的搜索效率對比

原文:http://www.dongwm.com/archives/ack/ 作者: 董偉明

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!