<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          改進限制太多的 Rust 庫 API

          共 1514字,需瀏覽 4分鐘

           ·

          2021-12-22 18:05

          在我之前的一篇文章“如何編寫 CRAP Rust 代碼[1]”中,我警告過不要過度使用泛型。對于二進制 crate 或任何代碼的初始版本,這仍然是一個好主意。

          然而,在設計 Rust 庫 crate API 時,你通??梢允褂梅盒蛠慝@得良好的效果:對我們的輸入更加寬容可能會為調(diào)用者提供避免某些分配的機會,或者以其他方式找到更適合他們的輸入數(shù)據(jù)的不同表示。

          在本指南中,我們將演示如何在不丟失任何功能的情況下使 Rust 庫 API 更加寬松。但在我們開始之前,讓我們檢查一下這樣做的可能缺點。

          首先,泛型函數(shù)為類型系統(tǒng)提供的關于什么是什么的信息較少。如果原來的具體類型現(xiàn)在變成了impl,編譯器將更難推斷每個表達式的類型(并且可能會更頻繁地失敗)。這可能需要你的用戶添加更多類型注釋來編譯他們的代碼,從而導致更糟糕的人體工程學。

          此外,通過指定一種具體類型,我們可以將函數(shù)的一個版本編譯到結(jié)果代碼中。使用泛型,我們要么付出動態(tài)調(diào)度的運行時成本代價,要么通過選擇單態(tài)化來[2]冒著使二進制文件膨脹的風險——在 Rust 術語中,我們選擇 dyn Trait vs. impl Trait。

          你選擇權衡哪一點主要取決于場景。請注意,動態(tài)調(diào)度有一些運行時成本,但代碼膨脹也會降低緩存命中率,從而對性能產(chǎn)生負面影響。一如既往,測量兩次,編碼一次。

          即便如此,對于所有公共方法,你都可以遵循一些經(jīng)驗法則。

          01 部分 traits

          如果可以的話,取一個切片 (&[T]) 而不是一個 &Vec (那個實際上有一個clippy lint[3])。你的調(diào)用者可能會使用一個 VecDeque,它有一個 .make_continuous() 方法,此方法返回一個 &mut [T]而不是一個 Vec,或者可能是一個數(shù)組。

          如果你還可以取兩個切片,VecDeque::as_slices可以在不移動任何值的情況下為你的用戶工作。當然,你仍然需要了解你的場景來決定這是否值得。

          如果你只取消引用切片元素,則可以使用&[impl Deref]. 請注意,除Deref之外,還有AsRef trait,它在路徑處理中經(jīng)常使用,因為std方法可能需要一個AsRef的引用轉(zhuǎn)換。

          例如,如果你使用一組文件路徑,&[impl AsRef]將使用比&[String]更多的類型:

          fn?run_tests(
          ????config:?&compiletest::Config,
          ????filters:?&[String],
          ????mut?tests:?Vec,
          )?->?Result<bool,?io::Error>?{?
          ????//?much?code?omitted?for?brevity
          ????for?filter?in?filters?{
          ????????if?dir_path.ends_with(&*filter)?{
          ????????????//?etc.
          ????????}
          ????}
          ????//?..
          }

          上式可以表示為:

          fn?run_tests(
          ????config:?&compiletest::Config,
          ????filters:?&[impl?std::convert::AsRef],
          ????mut?tests:?Vec,
          )?->?Result<bool,?io::Error>?{?
          //?..

          現(xiàn)在filters可能是String&str、 甚至Cow<'_, OsStr>的切片。對于可變類型,有AsMut。類似地,如果我們要求任何引用T在相等、順序和散列方面都與T它自身相同,我們可以使用Borrow/BorrowMut代替。

          那有什么意思?這意味著實現(xiàn) Borrow 的類型必須保證a.borrow() == b.borrow()、a.borrow() < b.borrow()a.borrow().hash(),如果所討論的類型分別實現(xiàn) Eq、OrdHash,則返回與 a == b、a < b ?和 a.hash() 相同。

          02 讓我們再重復一遍

          類似地,如果你只迭代 str 切片的字節(jié),除非你的代碼需要 UTF-8 strString 以某種方式來保證正常工作,否則你可以簡單地接受一個 AsRef<[u8]> 參數(shù)。

          一般來說,如果你只迭代一次,你甚至可以選擇一個 Iterator, 這允許你的用戶提供他們自己的迭代器,這些迭代器可能會使用非連續(xù)的內(nèi)存切片,將其他操作與你的代碼穿插在一起,甚至可以即時計算你的輸入。這樣做,你甚至不需要使項目類型泛型,因為如果需要,迭代器通??梢暂p松生成一個 T。

          實際上,如果你的代碼只迭代一次,你可以使用 impl Iterator>;如果你不止一次需要這些項目,需要使用一兩個切片。如果你的迭代器返回擁有的項目(item),例如最近添加的數(shù)組 IntoIterator,你可以放棄 impl Deref 并使用 impl Iterator。

          不幸的是,IntoIteratorinto_iter 會消耗 self,所以沒有通用的方法來獲取讓我們迭代多次的迭代器 — 除非,獲取 impl Iterator<_> + Clone的參數(shù),但 Clone 操作可能代價高昂,所以我不建議使用它。

          03 Into

          與性能無關,但通常受歡迎的是參數(shù) impl Into<_> 的隱式轉(zhuǎn)換。這通常會使 API 感覺很神奇,但要注意:Into 轉(zhuǎn)換可能很昂貴。

          盡管如此,你還是可以使用一些技巧來獲得出色的可用性。例如,使用 一個 Into>而不是一個 Option,將使用戶省略 Some。例如:

          use?std::collections::HashMap;

          fn?with_optional_args<'a>(
          ????_foo:?u32,
          ????bar:?impl?Into<Option<&'a?str>>,
          ????baz:?impl?Into<OptionString,?u32>>>
          )?{
          ????let?_bar?=?bar.into();
          ????let?_baz?=?baz.into();
          ????//?etc.
          }

          //?we?can?call?this?in?various?ways:
          with_optional_args(1,?"this?works",?None);
          with_optional_args(2,?None,?HashMap::from([("boo".into(),?0)]));
          with_optional_args(3,?None,?None);

          同樣,可能存在以成本高昂的方式實現(xiàn)的 Into> 類型。這是另一個例子,我們可以在漂亮的 API 和明顯的成本之間做出選擇。一般來說,在 Rust 中選擇后者通常被認為是符合 Rust 慣用法的。

          04 控制代碼膨脹

          Rust 將通用代碼單態(tài)化。這意味著對于你的函數(shù)被調(diào)用的每個唯一類型,將生成并優(yōu)化使用該特定類型的所有代碼的版本。

          這樣做的好處是它會導致內(nèi)聯(lián)和其他優(yōu)化,從而為 Rust 提供我們都知道和喜愛的出色性能品質(zhì)。但它有一個缺點,即可能會生成大量代碼。

          作為一個可能的極端示例,請考慮以下函數(shù):

          use?std::fmt::Display;

          fn?frobnicate_arrayconst?N:?usize>(array:?[T;?N])?{
          ????for?elem?in?array?{
          ????????//?...2kb?of?generated?machine?code
          ????}
          }

          即使我們只是迭代,也會為每個項目類型和數(shù)組長度實例化此函數(shù)。不幸的是,沒有辦法避免代碼膨脹以及避免復制/克隆,因為所有這些迭代器都在它們的類型中包含它們的大小。

          如果我們可以處理引用的項目,我們可以不調(diào)整大小并迭代切片:

          use?std::fmt::Display;

          fn?frobnicate_slice(slice:?&[T])?{
          ????for?elem?in?slice?{
          ????????//?...2kb?of?generated?machine?code
          ????}
          }

          這將至少為每個項目類型生成一個版本。即便如此,假設我們只使用數(shù)組或切片進行迭代。然后我們可以分解出依賴于類型的 frobnicate_item 方法。更重要的是,我們可以決定是使用靜態(tài)調(diào)度還是動態(tài)調(diào)度:

          use?std::fmt::Display;

          ///?This?gets?instantiated?for?each?type?it's?called?with
          fn?frobnicate_with_static_dispatch(_item:?impl?Display)?{
          ????todo!()
          }

          ///?This?gets?instantiated?once,?but?adds?some?overhead?for?dynamic?dispatch
          ///?also?we?need?to?go?through?a?pointer
          fn?frobnicate_with_dynamic_dispatch(_item:?&dyn?Display)?{
          ????todo!()
          }

          外部 frobnicate_array 方法現(xiàn)在只包含一個循環(huán)和一個方法調(diào)用,不需要太多的代碼來實例化。避免了代碼膨脹!

          通常,最好仔細查看方法的接口并查看泛型在何處被使用或丟棄。在這兩種情況下,都有一個自然邊界,我們可以在該邊界處分解出刪除泛型的函數(shù)。

          如果您不想要所有這些類型并且可以添加一點編譯時間,那么你可以使用我的momo[4] crate 來提取通用特征,例如 AsRefInto。

          05 代碼膨脹有什么不好?

          對于某些背景,代碼膨脹有一個不幸的后果:今天的 CPU 使用緩存層次結(jié)構(gòu)。雖然這些在處理本地數(shù)據(jù)時允許非??斓乃俣?,但它們對使用產(chǎn)生非常非線性的影響。如果你的代碼占用了更多的緩存,它可能會使其他代碼運行得更慢!因此,Amdahl 定律[5]不再幫助你在處理內(nèi)存時找到優(yōu)化的地方。

          一方面,這意味著通過測量微基準測試單獨優(yōu)化部分代碼可能會適得其反(因為整個代碼實際上可能會變慢)。另一方面,在編寫庫代碼時,優(yōu)化庫可能會使用戶的代碼變得更差。但是你和他們都無法從微基準測試中學到這一點。

          那么,我們應該如何決定何時使用動態(tài)分派以及何時生成多個副本?我在這里沒有明確的規(guī)則,但我注意到動態(tài)調(diào)度在 Rust 中肯定沒有得到充分利用!首先,它被認為性能較差(這并不完全錯誤,考慮到函數(shù)表查找確實增加了一些開銷)。其次,通常不清楚如何在避免分配的同時[6]做到這點。

          即便如此,如果測試表明它是有益的,Rust 可以很容易地從動態(tài)調(diào)度到靜態(tài)調(diào)度,并且由于動態(tài)調(diào)度可以節(jié)省大量編譯時間,我建議在可能的情況下開始動態(tài)調(diào)用,并且只有在測試顯示它時才采用單態(tài)要更快。這為我們提供了快速的運行時間,從而有更多時間改進其他地方的性能。最好有一個實際的應用程序來衡量,而不是一個微基準。

          我對如何在 Rust 庫代碼中有效使用泛型的介紹到此結(jié)束??炜鞓窐返厝ナ褂?Rust 吧!

          原文鏈接:https://blog.logrocket.com/improving-overconstrained-rust-library-apis/

          參考資料

          [1]

          如何編寫 CRAP Rust 代碼: https://blog.logrocket.com/how-to-write-crap-rust-code

          [2]

          單態(tài)化來: https://en.wikipedia.org/wiki/Monomorphization

          [3]

          T]) 而不是一個&Vec` (那個實際上有一個[clippy lint: https://rust-lang.github.io/rust-clippy/master/index.html#ptr_arg

          [4]

          momo: https://github.com/llogiq/momo

          [5]

          Amdahl 定律: https://en.wikipedia.org/wiki/Amdahl's_law

          [6]

          在避免分配的同時: https://llogiq.github.io/2020/03/14/ootb.html




          往期推薦


          我是 polarisxu,北大碩士畢業(yè),曾在 360 等知名互聯(lián)網(wǎng)公司工作,10多年技術研發(fā)與架構(gòu)經(jīng)驗!2012 年接觸 Go 語言并創(chuàng)建了 Go 語言中文網(wǎng)!著有《Go語言編程之旅》、開源圖書《Go語言標準庫》等。


          堅持輸出技術(包括 Go、Rust 等技術)、職場心得和創(chuàng)業(yè)感悟!歡迎關注「polarisxu」一起成長!也歡迎加我微信好友交流:gopherstudio

          瀏覽 64
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  免费成人毛片 | 久久色在线播放 | 欧美成人福利 | 最近最新MV字幕观看 | 猛操在线 |