2011年11月25日 星期五

Effective C++ item 30: 了解 inline

之前寫的《C++ inline 用法背後的原因》有一點小錯, 原來也有可在連結時期做 inline 的建置環境, 如 .NET CLI。另外提到 template 的實作通常要也要寫在 header 裡, 理由同 inline, 這樣 compiler 才知道怎麼具現化它。

此外, 這則條款提到 inline 的特性只是建議, 不是強制行為。並強調 inline 的缺點有

  • 若函式有點大且頻繁地使用, inline 後 object code 會變很大。可以想見會浪費硬碟空間和記憶體, 也可能更容易造成記憶體換頁而減低 instruction cache hit rate。
  • 在更改 inline 後的函式時, 使用到 inline 函式的 object code 必須重編。若沒用 inline 且是使用 dynamic linking 的話, 可以完全不動客戶端的程式。
  • 大部份 debugger 不支援設中斷點

第一點的負擔比較難評估, 第二、三點到是滿顯見的問題。所以結論是, 除簡單的 getter 或 max / min 這類小函式, 盡量別用 inline, 直到發覺某個小函式占據可觀比例的執行時間後, 再考慮使用 inilne。

2011年11月24日 星期四

ARM Instruction Set 初步心得

之前看了一點點 ARM 的東西, 做個記錄。我完全不熟這個領域, 下面的心得可能會有許多錯誤。

《Tonc: Whirlwind Tour of ARM Assembly》這篇超級詳細地從頭教怎麼寫 ARM assembly code。作者原本的用意是教人寫 GBA, 而 GBA 底層是跑 ARM, 所以會需要寫 ARM assembly code 最佳化。

看完這篇後再來看大方向的觀念《ARM架構》, 就很有感覺了。對一個大學時代只寫過一點 x86 assembly, 只在課本上看 RISC 的人來說, 讓我比較印象深刻的是

  • ARM 的指令相當精簡, 很容易懂。概念上來看, 硬體成本應該會較低較省電。
  • 為減少指令過於精簡不方便使用或效率差的負擔, 強化了一些功能。又一個好例子可用來說明系統設計是一連串的取捨
  • ARM 的 shift 沒負擔, 透過 Barrel shifter 可同時在 OP2 做 shift / rotate 等操作。
  • ARM 的所有指令都有另留幾個 bit 以支援 conditional code, 藉此減少需要猜測 branch 的負擔。猜錯 branch 得另外重取要用到的指令和資料, 時間成本似乎滿大的。這裡借用《Tonc: Whirlwind Tour of ARM Assembly》實作 max() 的範例程式表示什麼是 conditional code:
@ // r2= max(r0, r1):
@ r2= r0>=r1 ? r0 : r1;

@ Traditional code
    cmp     r0, r1
    blt .Lbmax      @ r1>r0: jump to r1=higher code
    mov     r2, r0  @ r0 is higher
    b   .Lrest      @ skip r1=higher code
.Lbmax:
    mov     r2, r1  @ r1 is higher
.Lrest:
    ...             @ rest of code
    
@ With conditionals; much cleaner
    cmp     r0, r1
    movge   r2, r0  @ r0 is higher
    movlt   r2, r1  @ r1 is higher
    ...             @ rest of code
  • 由於 ARM Instruction Set 希望全部指令大小一樣是 32 bits, 又拿了一些 bits 給 Barrel shifter 和 conditional code 用, 所以能放常數的 bits 又更少了。也不像 x86 那樣指令大小不同, 可以看 OP code 決定多一個 cycle 從下個 cycle 取出常數。所以, 只能讀一個 byte 大小的數字, 再配合 Barrel shifter 改變數值。所以, 可以讀入 0x1C000000, 0x001C0000, 但無法讀入 0x010C0000。有這種需求時, 得拆多個指令組起來, 或是放到附近的 constant pool。
  • 部份裝置主要讀取指令的匯流排寬度是 16 bits (如 GBA), 所以 32 bits 指令得花兩個 cycle 才能讀完。於是有 Thumb 的設計, 指令精簡為 16 bits, 不過也就沒有 conditional code 等加速的空間。但 Thumb 也有機會讓全部的程式變更小, 對嵌入式系統來說, 更小的執行檔表示可省下更多記憶體存執行檔, 藉此減少硬體成本。開發者可混用 ARM 和 Thumb, 達到時間和空間的平衡點。

2011年11月23日 星期三

超輕量級的 python web framework: Bottle

寫完上篇冗長的 Django 心得後, 改來介紹極端相反的 web framework: Bottle

有一則關於 Python 的笑話是這麼說的:

Python is the only language with more Web frameworks than keywords.

在初評估 web framework 時, 對照一大串 web framework 看到這則笑話, 實在是令我哭笑不得。不過在有不同需求後, 到覺得這樣也是有好處啦。

當初要做 ego-post 的時候, 想找個很方便部署的框架, 比較方便推廣, 最終目的是讓一般人也能用 (後來發覺寫成 Chrome App 更適合)。我的主要功能用 javascript 和 html5 (用到 localStorage) 實作, web 後端只是用來存長期資料而已。一開始是試 Flask, 但裝 Flask 需要另外裝 Werkzeug 和 Jinja 2, 所以最後改用 Bottle

Bottle 有多小呢? 全部就一個 bottle.py 而已, 所以懶得要求使用者另外裝 Bottle 的話, 將 bottle.py 加到目前專案就結束了。而且它不到 3000 行, 有興趣了解最基本的 web 應用需要那些功能, 看 bottle.py 應該可以學到不少東西。像 http response code 418 I'm a teapot 這種蠢東西, 就是從 bottle.py 裡學到的。

2011年11月21日 星期一

Effective C++ item 23: 以 non-member、non-friend 取代 member function

看到這條款有種水到渠成的快感, 來寫下心得。

在我大學寫 Java 四年的時光裡, 我寫了一堆 over design 假 OOP 的東西。於是, 在我出社會工作的三年裡, 我改用 Python 盡可能的都寫成 function, 覺得很卡時 (像是一堆參數重覆傳來傳去), 才會包成 class。想體會看看兩種極端的寫法, 藉此看看能否從中悟出什麼道理。

這七年的時光合起來, 我沒悟出什麼大道理, 到是覺得後來這種寫法偶而是有點不便, 不過大部份時候還挺順的, 不會包得太笨重。直到看到 Effective C++ item 23, 才有種恍然大悟的感覺, 明白背後的原因。

作者提出一個觀點: 愈少程式能動到資料, 封裝的效果愈好。所以, 若這個操作可以寫成 non-member、non-friend function, 可保證它不能存取到 private data。再者, 也比較有機會讓不同 class 來重用這個 function。像是 std 裡面的 sort(), 就比放到 vector 裡成為 member function 來得好。既可提高 vector 的封裝效果, 也方便重用 sort (有提供 index 和 comparable 即可)。這篇兼顧「減少相依性」和「提高重用性」切入說明這則條款的精髓, 值得一讀。

初看這個觀點有些反直覺, 還是會覺得放到 class 裡比較像「封裝」。要找函式時也比較方便, 看 class 的 header 就可知道全部操作。但 item 23 接著強調: 若要方便找函式, 放在 namespace 下也有類似的效果, 如同 Java 寫成 utility class 的 static member function 一樣。

回想 Java 的 Arrays.sort() 和 Collections.sort() 也是同樣的設計, 只是 Java 沒 namespace, 這類函式改放到某個 class 下。當然, Python 的 sorted() 也是如此, 不過是放在 __builtin__ 這個 package 裡。其它類型的還有 max()、min() 等函式。Python 和 Java 都有提供方便的 max()、min() 來操作 container, 不知 C++ 有沒有類似的東西, 只看到 algorithm 有比較兩個數字的實作。

2011-11-22 Update

Aethanyc 提醒, STL 裡有 max_element()

2011年11月19日 星期六

C++ 建構式和解構式使用 exception 的注意事項

item 08: Prevent exceptions from leaving destructor

一樣是要避免未定義行為, 清資源清到一半卻因 exception 而沒做完, 是不道德的, 有機會造成更多 memory leak。

《[17.9] How can I handle a destructor that fails?》指出更嚴重的問題, 當 exception A 發生時, C++ 不會執行剩下的程式, 而會跳到 catch A 的部份。但跳到 catch 前, 要先清掉這之中的 call stack, 自然會觸發各 function call 的 local variable 的 destructor。若在這時又丟出 exception B, 沒辦法決定該執行 catch A, 還是跳去執行 catch B。忠孝兩難全, 只好要求 process 自盡結束它短暫的一生。

所以說有 destructor 也不是都是好事, 像 Python / Java 沒法和你保證什麼時候會呼叫 "destructor" ( Python 的 __del__、Java 的 finalizer ), 自然不用擔心有這種 exception 中的 exception 兩難。只會另外建議你不要用 "destructor"

書中另外建議, 若想兼顧「自動回收資源」 (環保做功德) 和「避免在 destructor 內吞掉 exception」, 可另外提供方法讓使用者可呼叫函式清資源, 再自行處理可能丟出的 exception (像關 db connection)。若使用者忘了呼叫這類方法, destructor 再做最低限度的保護, 幫忙呼叫該函式清資源。不過有 exception 時, 使用者也沒得抱怨 destructor 吞掉 exception 沒處理。

那 constructor 內可不可以丟出 exception?

照理說要能丟比較何理, 不然怎麼處理 invalid argument? 查一下 FAQ, 果然有令人愉快的答案:

結論是不會有 memory leak, 也不會呼叫 destructor, 可以安心地丟 exception 出來。

C++ 建構式和解構式使用 virtual 的注意事項

原本打算快速掃過一遍《Effective C++ 3/e》, 有個大略印象, 之後再視需求仔細看相關守則, 但看到一半發現忘了一堆前面看過的東西。還是來寫寫筆記好了。

  • item 07: declare destructors virtual in polymorphic base classes。這樣 delete base pointer 時, 才會呼叫到真正的 destructor, 然後正確地依序往上呼叫各類別的 destructor。
  • item 09: never call virtual functions during construction or destruction。理由是 C++ 在 constructor / destructor 裡沒有多型的效果, 因為 C++ 認為在建構或解構時呼叫子類別的函式有危險, 有可能用到未定義的資料, 乾脆定成在 constructor / destructor 裡沒有多型的效果
  • 續上則, 補充說明原因。建構式的呼叫順序是由上往下, 而解構式是由下往上。所以若在建構式呼叫子類別的函式, 該函式可能用到子類別特有的欄位, 這時自然還沒初始化, 導致未定義行為。反之, 若在解構式呼叫子類別的函式, 子類別的解構式已清掉它的欄位, 這時再使用到也會導致未定義行為。
  • 續上則, 值得注意的是, 在 Java 裡沒有禁止這麼做。Java 類別欄位的 (class field) 的所有型別都有預設的初始值 (像是 0、false、null 等)。Java 任何時候呼叫任何函式, 都會有多型效果。在建構式或解構式時這麼做, 會呼叫到子類別的函式, 至於這類的行為是否符合需求, 就自行判斷啦。下面附上 Java 的測試碼, 順便測一下欄位和方法的行為, 注意兩者行為不同, 方法有多型效果。
class A {
 String m = "A.m";
 
 void p() { System.out.println("A.p(): " + m); }
 
 A() { System.out.println("A()"); p(); }
}

class B extends A {
 String m = "B.m";
 
 B() { System.out.println("B()"); p(); }
 
 void p() { System.out.println("B.p(): " + m); }
 
}

public class Test {
 public static void main(String[] args) {
  A a = new B();
  System.out.println("--- After new ---");
  a.p();
  System.out.println(a.m);
  System.out.println(((B)a).m);
 }

}
/*
執行結果:
A()
B.p(): null
B()
B.p(): B.m
--- After new ---
B.p(): B.m
A.m
B.m
*/

2011年11月5日 星期六

C++ inline 用法背後的原因

看過編出來的組語碼後, 深刻感受到 inline 可以省掉大量操作記憶體的時間, 特別是函式內容簡短又常被呼叫的話, 也許在 stack 上操作記憶體的時間比實際運算的還久。

初學 inline 語法讓我很困惑, 它是如此地令人困惑以致於 FAQ 裡有數則說明:

令人困惑的點在於, 無論是 non-member function 或 member function, 若要使用 inline, 函式的定義都要寫在 header 裡。否則 compiler 在編譯時會抱怨「undefined reference to ...」 -- 一個不明確的錯誤訊息。

直到看了 compiler 和 linker 運作的基礎知識後, 才明白是怎麼回事。原因是 compiler 一次處理一個 .cpp 檔以及它引入的 header (由前處理器引進來的, 所以其實還是只有處理一份原始碼檔案)。所以在使用 inline 且函式定義寫在另一個 cpp 檔時, compiler 無法進行 inline 最佳化。也許是為了方便實作 compiler / linker (或為了符合既有的工具?), 而如此設定 inline 的語法吧。

附帶一提, 看了《淺談 GCC 編譯技術 Break Compilation Boundaries with GCC》才知道 compiler optimization 有分不同層級, 像是 Link-time optimization。若要移除沒用到的程式, 就得在 link-time 才能確定某些程式真的沒有被用到。

在 Fedora 下裝 id-utils

Fedora 似乎因為執行檔撞名,而沒有提供 id-utils 的套件 ,但這是使用 gj 的必要套件,只好自己編。從官網抓好 tarball ,解開來編譯 (./configure && make)就是了。 但編譯後會遇到錯誤: ./stdio.h:10...