1 minute read     Posted on:     Updated on:     Views: Loading...

Benchmark Time Elapsed

讓我們先從簡單的一個例子看起

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import grpc
import time
from proto import echo_pb2
from proto import echo_pb2_grpc

# def benchmark(stub):
#     stub.Echo(echo_pb2.EchoRequest(input="2"))

def benchmark(stub, f):
    start_time = time.perf_counter_ns()
    stub.Echo(echo_pb2.EchoRequest(input="2"))
    end_time = time.perf_counter_ns()
    time_diff = end_time - start_time

    f.write(f"{i + 1} {time_diff}\n")

if __name__ == "__main__":
    round = 100000
    with grpc.insecure_channel('localhost:6600') as channel:
        stub = echo_pb2_grpc.EchoStub(channel)

        with open("grpc-benchmark.txt", "w") as f:
            for i in range(round):
                benchmark(stub, f)

以我個人來說,我很常會需要對我的實作進行所謂的 benchmark
常見的方法即為,測試目標 function 所需要花費的時間,上述我想要測試 gRPC 的平均呼叫時間
可以看到,為了要加入測試時間的程式碼,我不得不重新修改實作本身,讓它得以 擴展 它原本的行為
除了執行基本的 gRPC 呼叫,它還可以測試執行時間,並寫到特定的檔案內

某種程度上它很丑對吧
為了新增某個功能而改變原本的實作本身屬實是不太好的行為
這個篇章我們將一窺 Decorator Pattern 可以如何解決這個問題

有關 gRPC 的相關介紹,可以參考 網頁程式設計三兩事 - gRPC | Shawn Hsu
詳細的實作程式碼可以參考 ambersun1234/blog-labs/RESTful_gRPC_JSON-RPC-benchmark

Open-Closed Principle

Class should be open for extension, but closed for modification

這個原則出現於 Bertrand Meyer 所撰寫的物件導向書籍

一個模組或者說物件,要怎麼樣同時保有這兩種特性?
不願意被修改可以很好理解,如果要新增功能,我們可以藉由 inheritance(繼承) 自己實作新功能
但是你又說歡迎擴展? 不是不願意更改嗎?

回顧一下
一個物件由兩個基本的要素組成,屬性(data) 以及 行為(function)
一旦物件建立完成,我可以肯定的說,行為 是沒辦法輕易調整的對吧
屬性可以調整嗎? 答案是可以的

在 設計模式 101 - Observer Pattern | Shawn Hsu 裡面我們就有幹過類似的事情
我們是不是可以 動態的 新增/刪除觀察者?
這些觀察者是 屬性(data), 但是更改的同時,我們並沒有動到任何行為(function)

Decorator Pattern

我們可以更進一步的用 callback function 改進我們的 benchmark 程式

1
2
3
4
5
6
7
8
9
10
11
12
13
def time_elapsed(func):
    start_time = time.perf_counter_ns()
    func()
    end_time = time.perf_counter_ns()
    time_diff = end_time - start_time

    f.write(f"{i + 1} {time_diff}\n")

def benchmark():
    stub.Echo(echo_pb2.EchoRequest(input="2"))

# caller
time_elapsed(benchmark())

這本質上就是 Decorator Pattern 想達成的事情
我可以根據需要,動態的 加上額外的功能,而且我可以加很多層
且不會更改到原本的邏輯(即 Open-Closed Principle)

Object Oriented Programming

在 OOP 的世界裡,我們可以用比較 OO 的方法處理 Decorator Pattern
亦即不使用 function 包 function 的方式,而是採 class 包 class

設想我們有一個車輛訂購系統,使用者可以為他們的愛車新增選配(e.g. 行車記錄器、尾翼、避震 … etc.)
寫起來會長這樣

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from abc import ABC, abstractclassmethod

# interfaces
class CarInterface(ABC):
    @abstractclassmethod
    def cost(self) -> int: raise NotImplementedError

class EquipmentInterface(ABC):
    def __init__(self, car: CarInterface):
        self.car = car

    @abstractclassmethod
    def cost(self) -> int: raise NotImplementedError

# car
class Benx(CarInterface):
    def cost(self) -> int:
        return 100
    
# equipment
class Recorder(EquipmentInterface):
    def cost(self) -> int:
        return self.car.cost() + 12
    
class ShockAbsorber(EquipmentInterface):
    def cost(self) -> int:
        return self.car.cost() + 80
    
if __name__ == "__main__":
    car = Benx()
    car = Recorder(car)
    car = ShockAbsorber(car)

    print(car.cost())

上述的實作可以這樣解讀

1
2
3
4
整輛車的價格 = 改避震器的價格 + 其他1
其他1 = 行車記錄器的價格 + 其他2
...
依此類推

跟 divide and conquer 滿像的對吧?
老實說,這個方式跟非 OOP 的作法是一樣的,只不過我們是用 class 進行操作的


不論 OOP 與否,Decorator 的核心概念就是 動態的 新增額外功能,而不更改到原本的實作

Benchmark Time Elapsed with Decorator Pattern

Python 有內建提供給 Decorator Pattern 的 syntax sugar
當然,decorator 還是要由我們自己設計
於是可以改成最終完成版如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import grpc
import time
from proto import echo_pb2
from proto import echo_pb2_grpc

def time_elapsed(func):
    def measure_time():
        start_time = time.perf_counter_ns()
        func()
        end_time = time.perf_counter_ns()
        time_diff = end_time - start_time

        f.write(f"{i + 1} {time_diff}\n")

    return measure_time

@time_elapsed
def benchmark():
    stub.Echo(echo_pb2.EchoRequest(input="2"))

if __name__ == "__main__":
    round = 100000
    with grpc.insecure_channel('localhost:6600') as channel:
        stub = echo_pb2_grpc.EchoStub(channel)

        with open("grpc-benchmark.txt", "w") as f:
            for i in range(round):
                benchmark()

詳細的實作程式碼可以參考 ambersun1234/blog-labs/RESTful_gRPC_JSON-RPC-benchmark

採用 Decorator Pattern 之後,我既不會更改原本的實作,但我仍然可以擴充它,是不是很漂亮呢?

decorator 裡面的 i 與 f 都是取自 global variable scope
當僅僅讀取 global variable 的時候不需要使用 global xxx syntax

i 的 variable scope 不是存在於 for-loop, 而是存在於整個 function
可參考 Do iteration variable exist after the iteration statement in python? [duplicate]

if __name__ == "__main__" 的 variable scope 屬於 global
可參考 The scope of if __name__ == __main__

References

Leave a comment