愈一般化的程式碼,意謂著可以涵蓋更多的需求,達成更廣泛的作用。愈一般化的程式,通常也就代表有著愈高的通用性、也就有可能會有愈好的可重複使用性。

我們學習很多設計的技巧,就是希望能夠做出足夠一般化的設計,讓設計更為通用,大家所持續努力的,無非是想讓程式碼達到「一以貫之」的至高境界。足夠一般化的程式碼,通常對生產力都會有所幫助,為了解決生產力的問題,眾多的程式設計者都把撰寫「一般化」的程式碼列為重要的修煉目標。

另一方面,我們也會於自身應用程式的開發中,採用許多現成的程式庫與應用程式框架。當然,這些程式庫及應用程式框架,在設計上通常也都會朝著高通用性的目標發展,因此,「一般化」也是程式庫及應用程式框架在設計時的重點。

程式碼一般化會犧牲一些式執行效能
雖然不同類型應用程式開發者的需求不盡相同,但是因為這些程式庫及應用程式框架的設計足夠「一般化」,足以涵蓋各種應用上的變形,因而能為各應用程式開發解決問題,讓他們只要運用這些既存的輪子,就能據以創造出各種車子。有了這些輪子的問世,應用程式的開發生產力的確持續有所提升。

隨著大家愈來愈重視生產力問題,各式各樣的輪子也愈來愈多,各種應用程式開發時所需的基礎設施,幾乎都可以找到現成的程式庫或應用程式框架,使得開發者毋需自行重新打造。而且,這些程式庫和應用程式框架封裝的層次愈來愈高階,過去必須倚靠開發者自行建構的許多環節,如今都可由現成的程式庫和應用程式框架代勞。開發者可以更專注在自身的應用程式,不需要耗費額外的時間和心力,來自行建構應用程式的基礎設施。

從生產力的觀點來看,這樣的發展方向是對的。透過愈來愈高階、愈來愈一般化的程式庫和應用程式框架,解決眾多應用程式的共通基本問題,開發者可以專心面對更高階的應用層面問題。

不過,通常,「一般化」的特性,通常都會衍生出一個缺點,那就是運行效能的問題。

因為愈是「一般化」,就代表愈是「通用化」,那麼,能夠解決的問題也就愈為廣泛。因為問題輸入夠廣泛,所以解決方案勢必兼顧到廣泛的各種可能性,就無法利用較為特定的問題特性,來提供更有效率的解法。

舉例來說,一個計算二維空間內向量距離的程式,以及一個可以計算任意維度空間內向量距離的程式,相較起來,前者想要寫的高效,比起後者而言,可以說是簡單多了。因為前者不需要考慮到通用的型式,也就是輸入問題可能是各種維度中的向量,只需要專心對付二維空間中的向量即可。解法可以很特定,因此,如果依據輸入資料的特性,寫死一些行為及特質(例如,不需要利用動態配置的陣列,來記錄可能是任意維度的向量值),那麼自然有助於提升運行的效率。相反的,愈是「一般化」的程式碼,為了提供足夠的通用性,通常就會犧牲效率了。

「通用性」和「效率」通常就和魚與熊掌一樣,是無法兼得的。一般來說,我們會為了生產力,寧可犧牲效率,也希望得到生產力,因為效率可以倚靠硬體的進步來提升。相較而言,開發的生產力是個比較難輕易提升的特質。

現在有著眾多的程式庫及應用程式框架被發展出來,協助我們解決生產力的問題,而這些程式庫和應用程式框架也為了滿足眾多開發者的需求,都會盡可能的提供足夠一般化的解決方案,而程式庫及應用程式框架層層相疊,每一層都提供一定程度的抽象化及包裝,也都各自在效率上做了取捨。

開發者受限於程式庫與應用程式框架
另一方面,因為程式庫和應用程式框架提供了很好的便利性,讓程式設計者漸漸產生了依賴,甚至構成了另一種形式的「框架」,限制住程式設計者的思考。也就是說,程式設計者在不知不覺中,已經習慣了這些由程式庫和應用程式框架所提供的基礎設施,當他在開發時,很直覺地便會想到該運用那些程式庫和應用程式框架,來達成自己想要的目的。這樣並沒有什麼不對,但是,很多時候,程式設計者被這些現成的基礎設施給局限住了,一旦基礎設施出了問題,他們便束手無策。甚至,會認為那是既定的限制,難以改善。

而基礎設施最常見的問題,就是效率問題。正如前段文中所述,這些基礎設施有時候為了滿足「一般化」的需求,自然而然,沒有辦法在效率上專精地為特定的需求提供最佳化。

前一陣子我們在開發上遇到一個問題。我們舊有的架構,是在對關聯式資料庫存取層之上再加上一個記憶體快取層。我們使用十分普遍的memcached,加上Java版本的程式庫去,存取記憶體快取。

在過去的使用上,這樣的架構一直沒有什麼問題。但是,最近,使用同樣的架構在存取某種類型的資料時,問題重重。因為資料特性的關係,當我們將資料置入memcached或將資料自memcached中取出時,都會產生瞬間大量佔用記憶體的情況,而且效能相當差。當同時間有足夠多此種類型的工作任務在執行,就會讓Java的虛擬機器因為記憶體不足,而停止運作。

找到問題點之後,針對這種情況,我請同事思考如何改善。同事們左思右想,得到的答案是沒有辦法,因為將資料置入memcached,或將資料自memcached中取出,就是會有這種情況,而且要存取的資料量已經無法再降低了。

乍聽之下,這似乎頗有道理,但是同事似乎沒有思考到,若不用舊有存取memcached的方式,是否還有提升效能及降低每次操作時記憶體佔用量的空間呢?這就是既有使用程式庫的習慣所形成的一個框架,有時候,程式設計者就會被既有的框架給限制住思路,忽略了在這個框架之外,其實還有很多可行性供選擇。

後來我決定針對該特定的資料,捨棄原有memcached的操作方式,取而代之的,是專門為該特定資料所寫的快取機制。因為該資料有著很明確的資料特性,針對這個很專屬的資料特性,就有做最佳化的空間。

用特殊化來打破一般化帶來的限制
我們如果想要的是通用性,設計就要朝「一般化」的方向發展。相反的,如果我們想要的是執行效率,那麼設計上就要朝「特殊化」的方向去發展。因為只有進行特殊化,為專門的、限定的輸入資料做量身打造,才有辦法取回在「一般化」的過程中所損失的效率。

就像前面所舉的例子,如果你的問題其實只是在解二維空間內的向量問題,你卻使用了一個可處理任意維度中向量問題的程式庫,那麼就有可能因此而損失效率。為此,你可以自行設計或找到一個專解二維空間內的向量問題的程式,便可提升效率。

當我們使用自行撰寫、用途極其有限的特殊化版本的快取機制之後,不僅存取速度改善,佔用記憶體的情況也大幅降低,原有的問題可以說是因此而順利解決。

這是一個很好的例子,當我們習於使用現成的程式庫或應用程式框架來做為生產力的基礎時,千萬別忘了,在遇到問題,尤其是效率問題時,要跳出這些框架所構成的限制,做一些專門的特殊化設計,才能夠解決現成程式庫或應用程式框架先天上所無法解決的問題。

 

專欄作者

熱門新聞

Advertisement