42

私はPythonでredisキャッシュを作成したかったので、自尊心のある科学者として、パフォーマンスをテストするためのベンチマークを作成しました。

興味深いことに、redisはそれほどうまくいきませんでした。Pythonが何か魔法をかけている(ファイルを保存している)か、私のバージョンのredisが途方もなく遅いです。

これが私のコードの構造のせいなのか、それとも何なのかはわかりませんが、redisの方がうまくいくと期待していました。

redisキャッシュを作成するために、バイナリデータ(この場合はHTMLページ)を、有効期限が5分のファイル名から派生したキーに設定しました。

すべての場合において、ファイル処理はf.read()を使用して行われます(これはf.readlines()よりも約3倍高速であり、バイナリブロブが必要です)。

比較で欠けているものがありますか、それともRedisは本当にディスクに一致しませんか?Pythonはファイルをどこかにキャッシュし、毎回再アクセスしていますか?なぜこれがredisへのアクセスよりもはるかに速いのですか?

私はredis2.8、python 2.7、redis-pyをすべて64ビットのUbuntuシステムで使用しています。

ファイルデータをpythonオブジェクトに格納し、それを永久に生成する関数を作成したので、Pythonが特に魔法のようなことをしているとは思いません。

グループ化した4つの関数呼び出しがあります。

ファイルをX回読み取る

redisオブジェクトがまだメモリ内にあるかどうかを確認するために呼び出される関数、オブジェクトをロードするか、新しいファイル(単一および複数のredisインスタンス)をキャッシュします。

(redisのシングルインスタンスとマルチインスタンスを使用して)redisデータベースから結果を生成するジェネレーターを作成する関数。

そして最後に、ファイルをメモリに保存し、永久に譲ります。

import redis
import time

def load_file(fp, fpKey, r, expiry):
    with open(fp, "rb") as f:
        data = f.read()
    p = r.pipeline()
    p.set(fpKey, data)
    p.expire(fpKey, expiry)
    p.execute()
    return data

def cache_or_get_gen(fp, expiry=300, r=redis.Redis(db=5)):
    fpKey = "cached:"+fp

    while True:
        yield load_file(fp, fpKey, r, expiry)
        t = time.time()
        while time.time() - t - expiry < 0:
            yield r.get(fpKey)


def cache_or_get(fp, expiry=300, r=redis.Redis(db=5)):

    fpKey = "cached:"+fp

    if r.exists(fpKey):
        return r.get(fpKey)

    else:
        with open(fp, "rb") as f:
            data = f.read()
        p = r.pipeline()
        p.set(fpKey, data)
        p.expire(fpKey, expiry)
        p.execute()
        return data

def mem_cache(fp):
    with open(fp, "rb") as f:
        data = f.readlines()
    while True:
        yield data

def stressTest(fp, trials = 10000):

    # Read the file x number of times
    a = time.time()
    for x in range(trials):
        with open(fp, "rb") as f:
            data = f.read()
    b = time.time()
    readAvg = trials/(b-a)


    # Generator version

    # Read the file, cache it, read it with a new instance each time
    a = time.time()
    gen = cache_or_get_gen(fp)
    for x in range(trials):
        data = next(gen)
    b = time.time()
    cachedAvgGen = trials/(b-a)

    # Read file, cache it, pass in redis instance each time
    a = time.time()
    r = redis.Redis(db=6)
    gen = cache_or_get_gen(fp, r=r)
    for x in range(trials):
        data = next(gen)
    b = time.time()
    inCachedAvgGen = trials/(b-a)


    # Non generator version    

    # Read the file, cache it, read it with a new instance each time
    a = time.time()
    for x in range(trials):
        data = cache_or_get(fp)
    b = time.time()
    cachedAvg = trials/(b-a)

    # Read file, cache it, pass in redis instance each time
    a = time.time()
    r = redis.Redis(db=6)
    for x in range(trials):
        data = cache_or_get(fp, r=r)
    b = time.time()
    inCachedAvg = trials/(b-a)

    # Read file, cache it in python object
    a = time.time()
    for x in range(trials):
        data = mem_cache(fp)
    b = time.time()
    memCachedAvg = trials/(b-a)


    print "\n%s file reads: %.2f reads/second\n" %(trials, readAvg)
    print "Yielding from generators for data:"
    print "multi redis instance: %.2f reads/second (%.2f percent)" %(cachedAvgGen, (100*(cachedAvgGen-readAvg)/(readAvg)))
    print "single redis instance: %.2f reads/second (%.2f percent)" %(inCachedAvgGen, (100*(inCachedAvgGen-readAvg)/(readAvg)))
    print "Function calls to get data:"
    print "multi redis instance: %.2f reads/second (%.2f percent)" %(cachedAvg, (100*(cachedAvg-readAvg)/(readAvg)))
    print "single redis instance: %.2f reads/second (%.2f percent)" %(inCachedAvg, (100*(inCachedAvg-readAvg)/(readAvg)))
    print "python cached object: %.2f reads/second (%.2f percent)" %(memCachedAvg, (100*(memCachedAvg-readAvg)/(readAvg)))

if __name__ == "__main__":
    fileToRead = "templates/index.html"

    stressTest(fileToRead)

そして今、結果:

10000 file reads: 30971.94 reads/second

Yielding from generators for data:
multi redis instance: 8489.28 reads/second (-72.59 percent)
single redis instance: 8801.73 reads/second (-71.58 percent)
Function calls to get data:
multi redis instance: 5396.81 reads/second (-82.58 percent)
single redis instance: 5419.19 reads/second (-82.50 percent)
python cached object: 1522765.03 reads/second (4816.60 percent)

結果は、a)ジェネレーターが毎回関数を呼び出すよりも高速であり、b)redisがディスクからの読み取りよりも低速であり、c)Pythonオブジェクトからの読み取りが途方もなく高速であるという点で興味深いものです。

ディスクからの読み取りが、redisからのメモリ内ファイルからの読み取りよりもはるかに高速なのはなぜですか?

編集:いくつかのより多くの情報とテスト。

関数をに置き換えました

data = r.get(fpKey)
if data:
    return r.get(fpKey)

結果は大差ありません

if r.exists(fpKey):
    data = r.get(fpKey)


Function calls to get data using r.exists as test
multi redis instance: 5320.51 reads/second (-82.34 percent)
single redis instance: 5308.33 reads/second (-82.38 percent)
python cached object: 1494123.68 reads/second (5348.17 percent)


Function calls to get data using if data as test
multi redis instance: 8540.91 reads/second (-71.25 percent)
single redis instance: 7888.24 reads/second (-73.45 percent)
python cached object: 1520226.17 reads/second (5132.01 percent)

各関数呼び出しで新しいredisインスタンスを作成しても、実際には読み取り速度に目立った影響はありません。テストごとのばらつきはゲインよりも大きくなります。

Sripathi Krishnanは、ランダムなファイル読み取りの実装を提案しました。これらの結果からわかるように、これがキャッシングが本当に役立つところです。

Total number of files: 700

10000 file reads: 274.28 reads/second

Yielding from generators for data:
multi redis instance: 15393.30 reads/second (5512.32 percent)
single redis instance: 13228.62 reads/second (4723.09 percent)
Function calls to get data:
multi redis instance: 11213.54 reads/second (3988.40 percent)
single redis instance: 14420.15 reads/second (5157.52 percent)
python cached object: 607649.98 reads/second (221446.26 percent)

ファイルの読み取りには非常に大きなばらつきがあるため、パーセント差はスピードアップの良い指標ではありません。

Total number of files: 700

40000 file reads: 1168.23 reads/second

Yielding from generators for data:
multi redis instance: 14900.80 reads/second (1175.50 percent)
single redis instance: 14318.28 reads/second (1125.64 percent)
Function calls to get data:
multi redis instance: 13563.36 reads/second (1061.02 percent)
single redis instance: 13486.05 reads/second (1054.40 percent)
python cached object: 587785.35 reads/second (50214.25 percent)

random.choice(fileList)を使用して、関数を通過するたびに新しいファイルをランダムに選択しました。

誰かがそれを試してみたい場合は、完全な要点がここにあります-https://gist.github.com/3885957

編集編集:ジェネレーター用に1つのファイルを呼び出していることに気づいていませんでした(関数呼び出しとジェネレーターのパフォーマンスは非常に似ていましたが)。これは、ジェネレーターからのさまざまなファイルの結果でもあります。

Total number of files: 700
10000 file reads: 284.48 reads/second

Yielding from generators for data:
single redis instance: 11627.56 reads/second (3987.36 percent)

Function calls to get data:
single redis instance: 14615.83 reads/second (5037.81 percent)

python cached object: 580285.56 reads/second (203884.21 percent)
4

1 に答える 1

41

これはリンゴとオレンジの比較です。http://redis.io/topics/benchmarksを参照してください

Redisは効率的なリモートデータストアです。コマンドがRedisで実行されるたびに、メッセージがRedisサーバーに送信され、クライアントが同期している場合は、応答の待機がブロックされます。したがって、コマンド自体のコストを超えて、ネットワークラウンドトリップまたはIPCの料金を支払うことになります。

最新のハードウェアでは、ネットワークラウンドトリップまたはIPCは、他の操作と比較して驚くほど高価です。これはいくつかの要因によるものです。

  • メディアの生の遅延(主にネットワーク用)
  • オペレーティングシステムスケジューラのレイテンシ(Linux / Unixでは保証されません)
  • メモリキャッシュミスはコストがかかり、クライアントプロセスとサーバープロセスのイン/アウトがスケジュールされている間、キャッシュミスの可能性が高くなります。
  • ハイエンドボックスでは、NUMAの副作用

それでは、結果を確認しましょう。

ジェネレーターを使用した実装と関数呼び出しを使用した実装を比較すると、Redisへのラウンドトリップ数は同じではありません。ジェネレーターを使用すると、次のようになります。

    while time.time() - t - expiry < 0:
        yield r.get(fpKey)

したがって、反復ごとに1ラウンドトリップします。この関数を使用すると、次のことができます。

if r.exists(fpKey):
    return r.get(fpKey)

したがって、反復ごとに2ラウンドトリップします。ジェネレーターの方が速いのも不思議ではありません。

もちろん、最適なパフォーマンスを得るために、同じRedis接続を再利用することになっています。体系的に接続/切断するベンチマークを実行する意味はありません。

最後に、Redis呼び出しとファイル読み取りのパフォーマンスの違いについては、ローカル呼び出しとリモート呼び出しを比較しているだけです。ファイル読み取りはOSファイルシステムによってキャッシュされるため、カーネルとPython間の高速メモリ転送操作です。ここにはディスクI/Oは含まれていません。Redisを使用すると、往復の費用を支払う必要があるため、はるかに遅くなります。

于 2012-10-13T07:44:39.287 に答える