みーのぺーじ

みーが趣味でやっているPCやソフトウェアについて.Python, Javascript, Processing, Unityなど.

Cloud FunctionsとCloud Runの処理速度を比較する

CPUが律速となる処理を行うのに適したサーバーレス環境を選定するために,ベンチマークなどを行いました.候補はGCPのCloud FunctionsとCloud Runです.AWSやAzureなど他のプラットフォームは検証しません.

注意事項

今回の検証結果は,みーが2021/01/10に実行した結果であり,実行する時刻やリージョンなどの影響を受ける可能性があります.Cloud Functionsのドキュメントには,「 CPU 割り当てはおおよその値です。CPU クロック サイクルの実際の割り当ては、関数呼び出しによって少し変わることがあります。*1」と記載されているなど,GCPで提供されるリソースは同じサービスでも差が出る可能性があることを考慮してください.また,将来に提供されるリソースが変更される可能性があるため,この記事の結果の永続性は保証できません.

Cloud Functions

Cloud FunctionsはCPUとメモリを決まった組み合わせから選択することができます.2021/01/10現在,以下の選択肢が存在します.

メモリ CPU 料金/100 ms(Tier 1 料金)
128 MB 200 MHz $0.000000231
256 MB 400 MHz $0.000000463
512 MB 800 MHz $0.000000925
1,024 MB 1.4 GHz $0.000001650
2,048 MB 2.4 GHz $0.000002900
4,096 MB 4.8 GHz $0.000005800

Pricing  |  Cloud Functions Documentation  |  Google Cloud

そもそも,4.8 GHzのCPUが選べるのはとても魅力的です.調べたところベースクロックが4.8 GHzのCPUは見つかりませんでしたが,Core i7-10700Fのターボブーストの最大周波数に一致しました.こんな優秀なCPUが使えるのかと夢が膨らみます.

実際のパフォーマンスをPythonのBenchmarker.pyを用いた以下のソースコードを実行して測定してみます.

Benchmarker.py README

# main.py
import json
from cpuinfo import get_cpu_info
from benchmarker import Benchmarker


def run(request):
    with Benchmarker(2000*1000, width=20) as bench:
        s1, s2, s3, s4, s5 = "Haruhi", "Mikuru", "Yuki", "Itsuki", "Kyon"

        @bench("join")
        def _(bm):
            for i in bm:  # instead of xrange(N)
                sos = ''.join((s1, s2, s3, s4, s5))

        @bench("concat")
        def _(bm):
            for i in bm:
                sos = s1 + s2 + s3 + s4 + s5

        @bench("format")
        def _(bm):
            for i in bm:
                sos = '%s%s%s%s%s' % (s1, s2, s3, s4, s5)
    #
    info = get_cpu_info()
    return f"CPU = {json.dumps(info, indent=2, sort_keys=True)}"

# requirements.txt
py-cpuinfo==7.0.0
Benchmarker==4.0.1

これをCloud Functionsにデプロイして,HTTPリクエストを送信して実行し,ログを確認します.リージョンは全てasia-northeast1にしました.

結果は以下のようになりました.なお,見やすさのため一部省略した部分があります.また,手間がかかるので一部の設定の検証を省略しました.

256 MB 400 MHz

CPU = {
  "arch": "X86_64",
  "arch_string_raw": "x86_64",
  "bits": 64,
  "brand_raw": "unknown",
  "count": 2,
  "cpuinfo_version_string": "7.0.0",
  "family": 6,
  "flags": [... ],
  "hz_actual": [
    2700250000,
    0
  ],
  "hz_actual_friendly": "2.7003 GHz",
  "hz_advertised": [
    2700250000,
    0
  ],
  "hz_advertised_friendly": "2.7003 GHz",
  "model": 85,
  "python_version": "3.8.5.final.0 (64 bit)",
  "vendor_id_raw": "GenuineIntel"
}

## benchmarker: release 4.0.1 (for python)
## python version: 3.8.5
## python compiler: GCC 7.5.0
## python platform: Linux-4.4.0-x86_64-with-glibc2.27
## python executable: /layers/google.python.pip/pip/bin/python3
## cpu model: unknown # 2700.250 MHz
## parameters: loop=2000000, cycle=1, extra=0
## real (total = user + sys)
join 2.4157 2.1900 2.1600 0.0300
concat 2.9958 2.7100 2.7100 0.0000
format 2.9025 2.5900 2.5900 0.0000
## Ranking real
join 2.4157 (100.0) ********************
format 2.9025 ( 83.2) *****************
concat 2.9958 ( 80.6) ****************
## Matrix real [01] [02] [03]
[01] join 2.4157 100.0 120.2 124.0
[02] format 2.9025 83.2 100.0 103.2
[03] concat 2.9958 80.6 96.9 100.0

1,024 MB 1.4 GHz

CPU = {
  "arch": "X86_64",
  "arch_string_raw": "x86_64",
  "bits": 64,
  "brand_raw": "unknown",
  "count": 2,
  "cpuinfo_version_string": "7.0.0",
  "family": 6,
  "flags": [...  ],
  "hz_actual": [
    2700211000,
    0
  ],
  "hz_actual_friendly": "2.7002 GHz",
  "hz_advertised": [
    2700211000,
    0
  ],
  "hz_advertised_friendly": "2.7002 GHz",
  "model": 85,
  "python_version": "3.8.5.final.0 (64 bit)",
  "vendor_id_raw": "GenuineIntel"
}

## benchmarker: release 4.0.1 (for python)
## python version: 3.8.5
## python compiler: GCC 7.5.0
## python platform: Linux-4.4.0-x86_64-with-glibc2.27
## python executable: /layers/google.python.pip/pip/bin/python3
## cpu model: unknown # 2700.211 MHz
## parameters: loop=2000000, cycle=1, extra=0
## real (total = user + sys)
join 0.6989 0.7000 0.7000 0.0000
concat 0.8304 0.8000 0.8000 0.0000
format 0.7980 0.7900 0.7900 0.0000
## Ranking real
join 0.6989 (100.0) ********************
format 0.7980 ( 87.6) ******************
concat 0.8304 ( 84.2) *****************
## Matrix real [01] [02] [03]
[01] join 0.6989 100.0 114.2 118.8
[02] format 0.7980 87.6 100.0 104.1
[03] concat 0.8304 84.2 96.1 100.0

2,048 MB 2.4 GHz

CPU = {
  "arch": "X86_64",
  "arch_string_raw": "x86_64",
  "bits": 64,
  "brand_raw": "unknown",
  "count": 2,
  "cpuinfo_version": [
    7,
    0,
    0
  ],
  "cpuinfo_version_string": "7.0.0",
  "family": 6,
  "flags": [... ],
  "hz_actual": [
    2700397000,
    0
  ],
  "hz_actual_friendly": "2.7004 GHz",
  "hz_advertised": [
    2700397000,
    0
  ],
  "hz_advertised_friendly": "2.7004 GHz",
  "model": 85,
  "python_version": "3.8.5.final.0 (64 bit)",
  "vendor_id_raw": "GenuineIntel"
}

## benchmarker: release 4.0.1 (for python)
## python version: 3.8.5
## python compiler: GCC 7.5.0
## python platform: Linux-4.4.0-x86_64-with-glibc2.27
## python executable: /layers/google.python.pip/pip/bin/python3
## cpu model: unknown # 2700.397 MHz
## parameters: loop=2000000, cycle=1, extra=0
## real (total = user + sys)
join 0.5032 0.5000 0.5000 0.0000
concat 0.5893 0.5800 0.5800 0.0000
format 0.5761 0.5800 0.5800 0.0000
## Ranking real
join 0.5032 (100.0) ********************
format 0.5761 ( 87.3) *****************
concat 0.5893 ( 85.4) *****************
## Matrix real [01] [02] [03]
[01] join 0.5032 100.0 114.5 117.1
[02] format 0.5761 87.3 100.0 102.3
[03] concat 0.5893 85.4 97.8 100.0

4,096 MB 4.8 GHz

CPU = {
  "arch": "X86_64",
  "arch_string_raw": "x86_64",
  "bits": 64,
  "brand_raw": "unknown",
  "count": 2,
  "cpuinfo_version_string": "7.0.0",
  "family": 6,
  "flags": [ ...  ],
  "hz_actual": [
    2799983000,
    0
  ],
  "hz_actual_friendly": "2.8000 GHz",
  "hz_advertised": [
    2799983000,
    0
  ],
  "hz_advertised_friendly": "2.8000 GHz",
  "model": 79,
  "python_version": "3.8.5.final.0 (64 bit)",
  "vendor_id_raw": "GenuineIntel"
}

## benchmarker: release 4.0.1 (for python)
## python version: 3.8.5
## python compiler: GCC 7.5.0
## python platform: Linux-4.4.0-x86_64-with-glibc2.27
## python executable: /layers/google.python.pip/pip/bin/python3
## cpu model: unknown # 2799.983 MHz
## parameters: loop=2000000, cycle=1, extra=0
## real (total = user + sys)
join 0.4763 0.4800 0.4800 0.0000
concat 0.6557 0.6500 0.6500 0.0000
format 0.6132 0.6200 0.6200 0.0000
## Ranking real
join 0.4763 (100.0) ********************
format 0.6132 ( 77.7) ****************
concat 0.6557 ( 72.6) ***************
## Matrix real [01] [02] [03]
[01] join 0.4763 100.0 128.7 137.7
[02] format 0.6132 77.7 100.0 106.9
[03] concat 0.6557 72.6 93.5 100.0

結果と考察

CPUの情報については,CPUの設定を変更しても,概ね2.7-2.8 GHzの周波数という情報が取得されました.おそらく,400 MHzの設定を行うと,2.8 GHzのCPUを使って1/7の時間を割り当てられるのだと思われます.つまり,どの設定を選択しても使用されるCPUには大きな差はないことが分かりました.

Pythonのベンチマークの結果を整理すると以下のようになりました.T*Fは,実行にかかった時間とCPUの設定の周波数を掛けて正規化した値です.値が小さい程周波数あたりの計算量が多いことを示します.

CPU 400 MHz 1.4 GHz 2.4 GHz 4.8 GHz
Memory 256 MB 1024 MB 2048 MB 4096 MB
join 2.4157 0.6989 0.5032 0.4763
concat 2.9958 0.8304 0.5893 0.6557
format 2.9025 0.7980 0.5761 0.6132
T*F join 0.96628 0.97846 1.20768 2.28624
T*F concat 1.19832 1.16256 1.41432 3.14736
T*F format 1.161 1.1172 1.38264 2.94336

1.4 GHzまでは周波数と処理量が比例しますが,2.4 GHz以上の周波数では,処理量が期待されるほど増えませんでした.

CPUのモデルについて,ほとんどの設定ではFamily 6 Model 85であり,Skylake (server) と思われました.しかし,4.8 GHzの設定のみFamily 6 Model 79で,Broadwell (Server)と1世代古いCPUで実行されたことが示唆されました.concatとformatで2.4 GHzよりも4.8 GHzの方が遅くなっているのはCPUの世代の差が原因かもしれません.

4.8 GHzのFamily 6 Model 79 CPUの正体は分かりませんが,おそらくはBroadwellのインテル Xeon プロセッサーなのだと思われます.

Cloud Run

先程のmain.pyを,以下のDockerfileで関数にして,Cloud Runに Docker Imageをデプロイしました.

FROM python:3.8-slim-buster

WORKDIR /app
COPY . .

RUN pip install functions-framework
RUN pip install -r requirements.txt

CMD exec functions-framework --target=run

PythonはAlpineと相性が悪いので,slim-busterを使いました.Memoryは1024Miに,リージョンはasia-northeast1にしました.

vCPU 1

CPU = {
    "arch": "X86_64",
    "arch_string_raw": "x86_64",
    "bits": 64,
    "brand_raw": "unknown",
    "count": 2,
    "cpuinfo_version_string": "7.0.0",
    "family": 6,
    "flags": [...  ],
    "hz_actual": [
        2789450000,
        0
    ],
    "hz_actual_friendly": "2.7894 GHz",
    "hz_advertised": [
        2789450000,
        0
    ],
    "hz_advertised_friendly": "2.7894 GHz",
    "model": 79,
    "python_version": "3.8.7.final.0 (64 bit)",
    "vendor_id_raw": "GenuineIntel" 
}

## benchmarker: release 4.0.1 (for python)
## python version: 3.8.7
## python compiler: GCC 8.3.0
## python platform: Linux-4.4.0-x86_64-with-glibc2.2.5
## python executable: /usr/local/bin/python
## cpu model: unknown # 2789.450 MHz
## parameters: loop=2000000, cycle=1, extra=0

## real (total = user + sys)
join 0.4616 0.4600 0.4600 0.0000
concat 0.5031 0.5100 0.5100 0.0000
format 0.5861 0.5800 0.5800 0.0000

## Ranking real
join 0.4616 (100.0) ********************
concat 0.5031 ( 91.7) ******************
format 0.5861 ( 78.8) ****************

## Matrix real [01] [02] [03]
[01] join 0.4616 100.0 109.0 127.0
[02] concat 0.5031 91.7 100.0 116.5
[03] format 0.5861 78.8 85.8 100.0

vCPU 1で,Cloud Functionsの最高設定よりも計算速度が早いという驚きの結果が出ました.参考のため,vCPU 2でも実行してみました.

vCPU 2

CPU = {
    "arch": "X86_64",
    "arch_string_raw": "x86_64",
    "bits": 64,
    "brand_raw": "unknown",
    "count": 2,
    "cpuinfo_version_string": "7.0.0",
    "family": 6,
    "flags": [... ],
    "hz_actual": [
        2799990000,
        0
    ],
    "hz_actual_friendly": "2.8000 GHz",
    "hz_advertised": [
        2799990000,
        0
    ],
    "hz_advertised_friendly": "2.8000 GHz",
    "model": 63,
    "python_version": "3.8.7.final.0 (64 bit)",
    "vendor_id_raw": "GenuineIntel"
}

## benchmarker: release 4.0.1 (for python)
## python version: 3.8.7
## python compiler: GCC 8.3.0
## python platform: Linux-4.4.0-x86_64-with-glibc2.2.5
## python executable: /usr/local/bin/python
## cpu model: unknown # 2799.990 MHz
## parameters: loop=2000000, cycle=1, extra=0

## real (total = user + sys)
join 0.4632 0.4700 0.4700 0.0000
concat 0.4823 0.4800 0.4800 0.0000
format 0.6046 0.6000 0.6000 0.0000

## Ranking real
join 0.4632 (100.0) ********************
concat 0.4823 ( 96.1) *******************
format 0.6046 ( 76.6) ***************

## Matrix real [01] [02] [03]
[01] join 0.4632 100.0 104.1 130.5
[02] concat 0.4823 96.1 100.0 125.4
[03] format 0.6046 76.6 79.8 100.0

結果と考察

vCPU毎にベンチマークの実行時間をまとめると以下のようになりました.

vCPU 1 2
join 0.4616 0.4632
concat 0.5031 0.4823
format 0.5861 0.6046

1CPUしか使わないベンチマークなので,当たり前ですがvCPUは1,2でほとんど差がありませんでした.

vCPU 1の時はFamily 6 Model 79であり,Broadwell (Server)だと思われます.vCPU 2の時はFamily 6 Model 63であり,Haswell (Server)だと思われます.vCPU 2でformatが遅くなっているのはCPUの世代の差が原因かもしれません.

Cloud FunctionsとCloud Runを比較すると以下のようになりました.

CPU Cloud Functions 1.4 GHz Cloud Functions 4.8 GHz Cloud Run vCPU 1
join 0.6989 0.4763 0.4616
concat 0.8304 0.6557 0.5031
format 0.7980 0.6132 0.5861

Cloud Runは最高設定のCloud FunctionsよりもCPUが高速でした.とても驚きました.気軽に作ったDockerfileですが,Cloud Functionsよりも高速なのは,Cloud Runが使っているKnativeが凄いのかもしれません.

料金の検討

CPUの速度において,Cloud Functionsの最高設定よりもvCPU 1のCloud Runの方が早いので,4,096 MB 4.8 GHzのCloud FunctionsとvCPU 1のCloud Runで,1リクエストに1000msかかるCPUが律速となる関数を1,000,000回実行するための費用を計算しました.3,107円と2,981円となり,少しだけCloud Runの方が安いものの,ほとんど同じ金額でした.

f:id:atsuhiro-me:20210110225601p:plain:w360

CPUが律速となるため,Cloud RunのConcurrencyは2以上に上げても意味がない点を考慮すると,費用に対する計算量のコストパフォーマンスはCloud Run = Cloud Functions 4.8GHz < Cloud Functions 1.4 GHzと考えられます.

まとめ

CPUが律速となる処理において,

  • Cloud Functionsは,CPUの計算量のコストパフォーマンスは1.4 GHzまで同じ.2.4 GHz以上はコストパフォーマンスが悪い.

  • Cloud Runの計算速度は早いが,費用のコストパフォーマンスは悪い.

という結論になりました.