中如何计算工龄_在Substrate中如何计算交易权重
讀完Substrate區塊鏈應用的交易費用設計的小伙伴,應該掌握:
- 什么是權重(weight)
- 交易費用包括:基本費用,字節費用,權重費用
- 交易級別:Normal和Operational
- 如何自定義一個權重計算方法: #[weight = FunctionOf(...)]
- 如何使用固定權重值,#[weight = SimpleDispatchInfo::FixNormal(X)]
這篇文章會詳細介紹,如何合理地設計runtime中dispatchable function (用戶可調用的方法)的weight,學習這篇文章,希望大家可以掌握:
- 如何寫出合格的weight document;
- 如何使用benchmark幫助計算weight
0. 概要
作為一名runtime developer, 在weights和fees方面,我們應該:
- 最小化runtime function的復雜度和占用的計算資源;
- 精確地定義相關runtime function的權重(weight)
為了達到以上要求,我們應該:
- 在寫runtime時,盡量多參考和follow優秀的設計和寫法
- 在注釋中盡量寫清楚方法的時間復雜度
- 計算這些方法在真實世界中的成本(benchmark),并把它和時間復雜度結合在一起思考
1. Follow Runtime Best Practices
https://github.com/paritytech/substrate
2. Weights注釋
首先,我們應該為在runtime中的dispatchable方法完善關于weight的注釋。這不僅可以幫助我們確定weight值的設定,也可以幫助我們更有效地優化代碼。關于weight的注釋結果以時間復雜度表示的結果展示,例如:
O(A + logA + BlogC)2.1 注釋要寫什么
weight相關的注釋應當要包含對runtime方法的執行成本有明顯影響的部分。比如:
- 存儲相關的操作 (read, write, mutate, etc.)
- Codec(Encode/Decode)相關操作(序列化/反序列化 vecs或者大的結構體)
- search/sort等成本高的計算
- 調用其他pallet中的方法
- ...
2.2 舉例分析
對下面一段代碼進行weight注釋時的分析:
// Join a group of members. fn join(origin) {let who = ensure_signed(origin)?;let deposit = T::Deposit::get(); // configuration constantlet sorted_members: Vec<T::AccountId> = Self::members();ensure!(sorted_members.len() <= 100, "Membership Full");match sorted_members.binary_search(&who) {// User is not a member.Err(i) => {T::Currency::reserve(&who, deposit)?;members.insert(i, who.clone());<Members<T>>::put(sorted_members);Ok(())},// User is already a member, do nothing.Ok(_) => Ok(()),}Self::deposit_event(RawEvent::Joined(who)); }Storage和Codec操作
訪問存儲是一個成本很高的操作,所以我們應當寫好注釋并優化。
每一個存儲操作都應當結合相關的Codec復雜度,寫好注釋。
比如,如果你需要從一個存儲項中讀取一個vec中的值,weight應該這樣寫:
- One storage read to get the member of this pallet: `O(M)`在這個例子中,在存儲中讀取vec有一個codec復雜度O(M),因為要對member M 進行反序列化操作。
稍后在module中,可能還會把一個數據再寫入存儲中,這也應該要有對應的注釋:
- One storage write to update the members of this pallet: `O(M)`Search, Sort 以及其他昂貴的計算
如果在runtime中需要搜索或者排序的話,同樣也需要標注相應的復雜度。比如,如果你在一個已經排序的list中執行搜索,binary_search 操作的時間復雜度為O(logM), 如果是一個未經排序的list的話,復雜度為O(M)。所以注釋應當像下面這樣寫:
- Insert a new member into sorted list: O(logM)調用其他pallet和trait
如果你調用其他FRAME pallet的方法,直接調用或者通過trait設置,需要記錄調用的那個方法的復雜度。比如,如果你寫的方法保留了一下余額(在Balances里)或者發送一個event(通過System pallet),你再注釋里應該寫上:
- One balance reserve operation: O(B) - One event emitted: O(E)最終的注釋
把以上的操作注釋結合在一起,我們就可以為一個方法寫上完整的注釋:
# <weight> Key: M (len of members), B (reserve balance), E (event) - One storage read to get the members of this pallet: `O(M)`. - One balance reserve operation: O(B) - Insert a new member into sorted list: O(logM). - One storage write to update the members of this pallet: `O(M)`. - One event emitted: O(E)Total Complexity: O(M + logM + B + E) # </weight> 注意: 在為方法寫weight注釋時可能引入了不同的參數,記得吧每個參數都標記好。如果仔細看上面的樣例代碼,就可以看到有兩個操作的時間復雜度都是O(M)(存儲讀和寫),但是整體的時間復雜度O并沒有把這部分考慮進來。
所以我們沒法區分有同樣復雜度的兩個方法,這意味著可以在這個方法里加入很多復雜度為O(M), O(logM)... 的操作,但是并不會對最后的復雜度標記做任何更改:
weight(M, B, E) = K_1 + K_2 * M + K_3 * logM + B + E對這部分區別,我們通過on-chain tests來衡量。
3. 如何大致推斷weight
總結:綜合考慮時間復雜度和具體的操作,參考目前FRAME中一些標志性的extrinsic(比如transfer),大致確定當前方法的weight數量級對weight有了更深的了解之后,在測試之前,我們可以給runtime方法先設定一個暫時的權重值。更多時候,我們的extrinsics大概率都是normal transactions,所以關于權重的聲明大概率會是像下面這樣:
#[weight = SimpleDispatchInfo::FixedNormal(YOUR_WEIGHT)]在深入探討更加細致的方法之前,我們可以簡單地參考一下現有pallet中方法的weight,給大家一個簡單的參考:
- System: Remark - 沒有任何邏輯。就用權重值允許的最小值標注。
- Staking: Set Controller. - 一次固定復雜度的存儲讀+寫 (500,000)
- Balances: Transfer. - 固定的時間復雜度 (1,000,000)
- Elections: Present Winner - O(voters) 復雜度的計算加上一次寫操作 (10,000,000)
- 如果想要在一個執行方法A的extrinsic最終會執行方法B,那么會簡單地在B的權重值的基礎上加上10_000,作為passthrough weight。 比如sudo中的sudo方法,詳見:
https://github.com/paritytech/substrate/blob/master/frame/sudo/src/lib.rs#L123
https://github.com/paritytech/substrate/pull/4946
4. 如何計算weight
給runtime中的某個方法定一個權重值,是一件有些主觀的事情,思考的維度也有很多,這里重點從技術角度,介紹如何給runtime中的方法進行性能測試,可以為確定方法的權重值多一些事實參考。
一般來說,想要給一個方法一個確定的權重值,可以從以下幾個角度考慮:
- 如果一個區塊里只包括這一個方法的extrinsic,你希望最多包括多少筆;
- 如果以balances.transfer作為基準,那這個方法要用多少權重;
- ...
從不同的側重點觸發,可能得到的結果也會稍有不同。這里只介紹最后一種思考方式,如果我們相信Substrate目前FRAME中的function weight是合理的話 。
4.1 benchmark
簡單來說,benchmark就是測試在指定的上下文中,執行指定的runtime function 花費了多少時間(in nanosencond)。benchmark介紹
Substrate中有關于benchmark的宏,benchmarks!,大家直接就可以使用。
使用benchmarks!的整體結構如下,
benchmarks! { _ {} scenarioA {}: functionA(...) scenarioB {}: functionA(...) scenarioC {}: functionC(...) }像_, scenarioA, scenarioB, scenarioC這部分,被稱為arms,可以簡單地理解為性能測試中的場景/分支??梢葬槍σ粋€runtime function構建多種場景,比如最好情況、一般情況、最壞情況等;而后面的functionA, functionB就是構建完場景(上下文)后具體執行的runtime function。如果該方法和arms重名的話,可以用_代替省略。
建議大家在性能測試時盡可能保守,即考慮最壞情況來確定weight關于構建測試場景(上下文),大家可以參考substrate現有的做法,比如:
- 輸入參數可以構造成升序、降序、隨機;
- 盡可能執行function中盡可能多的operations,比如轉轉賬時涉及到創建/清空地址;
- ...
benchmark示例參考:
benchmark示例參考:
https://github.com/paritytech/substrate/blob/master/frame/balances/src/benchmarking.rs
https://github.com/paritytech/substrate/blob/master/frame/identity/src/benchmarking.rs
暴露benchmark runtime api
在impl_runtime_apis中,給實現了benchmarking的pallet添加對應的benchmark接口;
impl_runtime_apis! {impl frame_benchmarking::Benchmark<Block> for Runtime {fn dispatch_benchmark(module: Vec<u8>,extrinsic: Vec<u8>,steps: Vec<u32>,repeat: u32,) -> Option<Vec<frame_benchmarking::BenchmarkResults>> {use frame_benchmarking::Benchmarking;match module.as_slice() {b"pallet-balances" | b"balances" => Balances::run_benchmark(extrinsic, steps, repeat).ok(),b"pallet-timestamp" | b"timestamp" => Timestamp::run_benchmark(extrinsic, steps, repeat).ok(),_ => None,}}} }benchmark CLI
最后一步,為了使substrate CLI可以使用類似substrate benchmark (或者node-template benchmark)這樣的subcommand來執行指定pallet的性能測試,我們還需要為cli添加對應的subcommand以及暴露benchmark host functions。因為目前node-template不是默認支持CLI中使用benchmark,所以我們需要對node-template做一些必要的微調:
具體做法請參考:
https://github.com/hammewang/substrate-multisig/?github.com之所以在substrate官方的node-template中沒有集成benchmark,也是Parity考慮后的結果:
https://github.com/paritytech/substrate/pull/4875
benchmark CLI如何使用
以multisig-template舉例:
./target/release/node-template benchmark --chain dev --pallet multisig --extrinsic create_multisig_wallet --execution wasm --repeat 2 --steps 10這里會把測試結果寫成一個csv,方便后續統計。補充說明一下常用參數:
- --execution wasm: 在wasm環境中執行性能測試,這里支持的所有可能的參數:[Native, Wasm, Both, NativeElseWasm]
- --pallet: 想要測試的pallet,這里可以填pallet-multisig或者multisig
- --chain: 鏈運行的初始狀態,比如dev, local,或者在chain_spec 自定義
- --steps: 從默認(1)開始,執行的最多樣本點數量
- --repeat: 每個樣本點重復次數
4.2 如何得到相對準確的weight
在執行完上述命令,會得到如下結果:
Pallet: "multisig", Extrinsic: "create_multisig_wallet", Steps: [10], Repeat: 2 u,extrinsic_time,storage_root_time 1,418000,39000 1,217000,29000 100,200000,28000 100,200000,29000 199,203000,29000 199,195000,27000 298,201000,28000 298,202000,28000 397,197000,28000 397,196000,29000 496,201000,28000 496,244000,28000 595,234000,31000 595,199000,28000 694,231000,32000 694,246000,29000 793,218000,31000 793,196000,28000 892,205000,29000 892,222000,29000 991,244000,32000 991,228000,31000根據第一行head可以看到,第二列就是執行這個extrinsic,在指定執行環境中,所花費的時間。大家可以根據需要對其進行處理,比如取平均值。
然后,大家可以在自己的機器上,再執行一遍balances.transfer的benchmark(我們選擇了balances.transfer的weight作為基準參考)。
然后把兩個性能測試的結果做一下對比,大致就可以得到一個相對準確的weight值。
再次友情提醒:請大家注意性能測試時,為了準確,覆蓋盡可能多的情況(最好、最壞、一般)這里貼一下Substrate目前已經有的部分benchmark結果:
補充
目前Substrate中的benchmark仍然處于早期狀態,后期很可能會出現breaking changes。本文寫于f41677d0, 想直接使用的童鞋請認準依賴版本。
希望可以給大家提供到一些,關于weight設定的基本概念,以及可行但粗糙的實踐方法。歡迎共同交流。
最新動態
github repo和substrate官方dev文檔是更新最及時、也是知識結構最系統的地方,值得star和收藏:
https://github.com/paritytech/substrate?github.comhttps://substrate.dev/?substrate.dev或者中文資料:
http://subdev.cn/?subdev.cn參考資料
https://substrate.dev/docs/en/conceptual/runtime/weight
https://github.com/paritytech/substrate/pull/3157
總結
以上是生活随笔為你收集整理的中如何计算工龄_在Substrate中如何计算交易权重的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: php中的控制器是什么意思,理解PHP中
- 下一篇: Intel发布30.0.101.1735