顯示包含「Programming」標籤的文章。顯示所有文章
顯示包含「Programming」標籤的文章。顯示所有文章

2012年6月26日星期二

程式師的修練場 - 建立自己的Junkcode及測試程式庫

大概很少人聽過junkcode這個字,如果在Google上尋找,首先會連到www.samba.org/junkcode,那是Andrew Tridgell(Samba的原作者)的網站,存放了他的垃圾代碼。

之所以被稱為Junk,那是因為他並未有計劃把這些代碼變成任何項目、也不會提供文件、更加不會保證有任何的改良。

Andrew Tridgell在大概八九年前左右曾來港演說,時歷久遠,當時的內容大多忘記,不過有二點是現在仍記得的,其中一項就是他提到的Junkcode程式庫。(註一)

受到他的影響,之後我也開始了建立自己Junkcode程式庫的習慣,只是沒有像他般公開。

我的Junkcode主要有幾種內容 :
  1. 不同語言的Hello World及語法的實驗。
  2. 不同Library/Framework的試驗
  3. Algorithm驗証
  4. 試作性質的專案
只有空閒時會撰寫, 不一定有明確的計劃,絕不保證日後有用,所以這些都是Junkcode。

因為沒責任,故此寫起上來是挺輕鬆的,除非目的是研究怎樣改良代碼,否則什麼代碼質素、易讀性的考慮都被拋諸腦後。

不過即使是垃圾,難保日後會有用武之地,所以我都有好好地做version control作為保存的手段,只是commit log就有點... 嘿嘿。

最初用CVS管理、後來git面世了,跟著就轉了去git,最後又變成了Bazaar。

像是FrontviewDQuestPenPenDualless這類項目的雛型都是先在名為junkcode的source tree中開始,當漸漸成型後才獨立分離出來,故此你若查看PenPen、Dualless的提交歷史時會發現第一個版本已經包含了許多的檔案,那都是先寫在junkcode裏的。

不過因為太隨意建立專案的關係,source tree變得有點混亂,所以我開始了使用Test Driven的方式管理。

Test Driven式學習

Test Driven Development是近年一個熱門題目,當初學習時的實驗代碼全都是在junkcode裏進行,掌握這方法後不單影響了我的專案管理風格,連Junkcode的寫法也有所變化。

例如説要去學習一門新的語言時,我首先寫的不是Hello World,而是先找一個評價好的Unit Test Framework,然後用這語言寫的第一個程式不會再説Hello,祇會説Pass / fail。

假設有以下的情況,我現在要去學Javascript,找來了一本叫做《Javascript設計模式》的書本,跟著有朋友推介使用QUnit做unit test,花了點時間總算把環境建立起來。

在書本中見到以下的代碼:


var a = [3];
console.log(a.length); // 1
consloe.log(a[0]); //3
 
var a = new Array(3);
console.log(a.length); // 3
consloe.log(typeof a[0]); //undefined


照著書本把代碼打一遍是一種學習的過程,只要查看注解的内容就知道是否正確,若印出預期結果可以加深記憶;失敗則能鍛練除錯的技巧。

而我則喜歡更進一步,把教學用代碼變成測試條件,例如以上代碼會變成:


test("array" , function() {
    var a = [3];
    ok(a.length == 1);
    ok(a[0] == 3);
 
    var a = new Array(3);
    ok(a.length == 3);
    ok(typeof a[0] == undefined);
});


跟著那去browser跑... 噢!第4個測試不能通過!?


ok(typeof a[0] == undefined);


這句不對嗎?

打錯了,原來是這様才對


ok(typeof a[0] == "undefined");


又或者這様也對


ok(a[0] == undefined);


在這轉换過程中不小心做出了超越作者預期的行為而犯錯,測試程式亮起了紅燈,跟著為了回復為綠燈去工作,這就是學習(而且帶了有一點兒TDD味道)

這種學習方法不限於學習語言本身,也可以用來學用不同的Library/framework、設計模式,可以簡單地驗查自己的想法是否正確,順道可以協助掌握寫測試程式的方法,對日後其他的專案開發很有幫助。

總結 

無讑是編程的初心者還是經験豐富的老手,建立自己的junkcode程式庫都有一定的好處,這些積累的作品不一定可以發表,可是……
  1. 或許日後會有機會用到,成為其他專案的一部份
  2. 或許可以在Junkcode中找回以解決過的問題方法,但現在忘了。
  3. 回顧以往寫過的代碼,也是一種樂趣
而且編寫junkcode的負擔很少,因為你不需要向任何人負責。

如果加入VCS及TDD,則還有以下好處:
  1. 練習使用版本控制系統
  2. 掌握各種測試技巧、有助於引入TDD
  3. 更快掌握一門語言、Library/Framework。
    1. 通過Test Driven式的學習,習慣思考不同行為的正確結果
  4. 重新掌握一門語言、Library
    1. 太久沒有使用相關的技術,可以查看過去寫的測試代碼回想起來。  
    2. 如果framework更新了,跑一次測試程式就可以知道有沒有跟過去的認知衝突的地方。

註一: 另一項記憶悠新的内容是關於在開發Samba時,曾經比較過Multi-process、Multi-threading及I/O multiplexing三種多工作業的效能,然後發現I/O multiplexing的效能是最好的,現在非常之熱門的Node.js其實也是應用了相同的原理。

2011年1月7日星期五

[C++編程心德] Config Class

寫應用程式最常遇到的功能之一是設定界面,讓用戶可以按他們的喜好改變軟件的運作方式。

在MVC的Design pattern慣例下,一般都會建立一個Config class去統一儲存所有的選項,像是以下的程式碼就很普遍。


class SimpleConfig {
public:

    void save();
    void load();

    void setOptionA(int val);
    int getOptionA();

    void setOptionB(double val);
    double getOptionB();

    void setOptionC(bool val);
    bool getOptionC();

private:
    int optionA;
    double optionB;
    bool optionC;
};

我也寫過類似的,不過很快就發覺那樣做很傻,每次增加選項都要c&p一堆代碼,然後當選項增至百來項時……嘿, 擁有數百個成員函數的Class看起來跟怪物沒二樣。

自從見識過幾次後,我就痛定思痛決不讓怪物再次出現。

事實上要改良以上的代碼其不困難,只要懂得使用Variant及Enum type。

Variant type

Variant type本身是一種資料類型,卻沒有固定的儲存方法,它可以是int、也可是double,就算是String或其他複雜資料類型也沒問題,雖然執行效率會比primitive type為低,使用起來卻非常方便,不過可惜C/C++並不支援。

故這得依懶其他library framework去實現,像gtk+及Qt這類都有各自的Variant type,除非堅持要寫純C++,否則就別自尋煩惱吧。

Enum type

為每個選項加入setter/getter會無可避免地令Class變得臃腫, 如果把Enum及Variant配合使用,就能大大簡化程式碼的複雜度:

class SimpleConfigImproved {
public:
    enum Option {
OptionA,
        OptionB,
        OptionC,
        Last
    } ;

    void save();
    void load();

    QVariant get(Option option);
    void set(Option option,QVariant val);

private:
    map <Option,QVariant> table;
};

註:QVariant為Qt的variant type

所有選項都用一個Enum的值去代表,只有一個setter及一個getter,每次增加選項都只不過是在宣告裏加一行代碼而已,這比之前的版本簡潔許多。

儲存

另外有一個以上未有考慮的事情就是儲存的方法,每個選項都要有一個名字,通常都會跟程式一樣,例如以上的程式碼可能會這樣儲存:
OptionA=0
OptionB=0.0
OptionC=false
問題是C/C++並沒有提供直接的方法可以讓你把Enum的值變成文字,你必須要找一個辦法讓程式碼中的”OptionA”能轉化成文字,否則就可以要寫下那麼愚蠢的程式:
write(“OptionA”,table[OptionA]);
write(“OptionB”,table[OptionB]);
write(“OptionC”,table[OptionC]); 
每加一個選項時記得再C&P一次......

關於這個問題有許多的方法去解決,基於我是個懶人,而最近也在寫Qt,在Qt裏只要加上Q_ENUMS宣告就能幫你把Enum的值變成QString,用很簡單的程式碼就能完成儲存的工作:
   for (int i = OptionA ; i < Last ; i++ ){
        write( nameOfOption( (Option) i) );
    }
加多少選項都不用重寫儲存的程式碼! 

宣告

class ConfigQt : public QObject {
    Q_OBJECT
    Q_ENUMS(Option)

public:
    enum Option {
        OptionA,
        OptionB,
        OptionC,
        Last
    } ;

    void save();
    void load();

    QVariant get(Option option);
    void set(Option option,QVariant val);

    static QString nameOfOption(Option val);

private:
    QMap <Option,QVariant> table;
};

QString ConfigQt::nameOfOption(ConfigQt::Option val) {
    QMetaEnum metaEnum = staticMetaObject.enumerator(0);
    return metaEnum.key(val);
}

 
 

2010年7月22日星期四

解讀Valgrind的訊息(1)

C/C++是一個沒有garbage collection的語言,動態呼叫所佔用的記憶體必須自行處理,一旦處置不當便會產生memory leakage及dangling pointer等各種問題,Memory leakage若非嚴重是有機會不影響程式的運作,而dangling pointer在大多情況下也可以很容易地用debugger找出來(不保證就是了)。

可是若發生memory的invalid read/write問題如heap block overrun之類,用debugger也不一定找得出來,錯誤的程序不會立即造成問題,就像武林高手用內功震傷對手後潛勁不會立即發作,而是隨時間加深傷勢,忽然間就爆發出來。

很多時程式會在不固定的地方crash,又或者在不同的機器上出現不同的表現,更常見是Debugger環境下執行及實際執行的結果有所出入,實際執行時是在A的地方出錯,但在debugger下卻在B的地方停下來,解決這些問題往往令人抓狂,這也是C/C++語言令人恐懼的原因之一。

這時候就要祭出終極武器Valgrind來幫你疏通經脈,這是一個有關於記憶體運用的除錯器、尋找洩漏、效能評估的工具,它能監控程式一切的運作,令記憶體有關的問題無所遁形。

不過呢,越是強大的工具就往往越難操作,Valgrind正是屬於這類,主要有二個問題

1)大量的假陽性的報告
很多時候都會回報大量的錯誤報告,但實際上都是對程式沒有影響的

2) 訊息的說明不足、難以理解
以前Valgrind的說明書是很簡短的,現在好了很多,但訊息並不一定直接告訢你問題的成因,只能大概告訢你這裏有些錯誤,加上有(1)提到的問題,怎樣找出有用的訊息及正確地解讀就要看閣下的功力。
這二星期不幸地都要祭出valgrind除錯,剛好有些能公開的代碼,所以就試試解說其中一項訊息,以後如果還遇上的話可以再說多一點,所以文章會分幾部份,但什麼時候有下一集就不得而知了。

這次會說的訊息是:

Address 0x8015be4 is 4 bytes inside a block of size 52 free'd

問題程式:PenPen (一個繪圖程式)
平台:N900
繪圖庫:Qt
取得代碼: bzr branch -r 29 lp:penpen

症狀:

1) 在進行了Refactor及增加一些功能後,在任一幅圖畫上繪畫,然後開啟另一幅畫,這時候會segfault

2) 如果光是觀看圖畫,不修改,則無論重覆多少次打開及關閉也沒有問題

3) 在GDB裏會見到是以下代碼出錯:
class QGraphicsSceneFindItemBspTreeVisitor : public QGraphicsSceneBspTreeVisitor
{

    QList *foundItems;
    bool onlyTopLevelItems;

    void visit(QList *items)
    {
        for (int i = 0; i < items->size(); ++i) {
        QGraphicsItem *item = items->at(i);
        if (onlyTopLevelItems && item->d_ptr->parent) // 在這裏出錯,存取item導至segfault
            item = item->topLevelItem();
            if (!item->d_func()->itemDiscovered && item->d_ptr->visible)     {
                item->d_func()->itemDiscovered = 1;
            foundItems->prepend(item);
            }
    }
    }

};
4) 看stack trackback會發現程式並沒有直接呼叫過QGraphicsSceneFindItemBspTreeVisitor::visit(),是經event loop去呼叫的,這是Qt的內部function。

初步診斷:

  1. 因為在Refactor前沒有問題,應該不是Qt的bug
  2. 因為沒有直接呼叫有關的function,是因為不當的API呼叫程序的可能性不高
  3. 已檢查過refactor前後的代碼修改,不覺得有出錯的地方,因為程式在自己沒有觸及的地方segfault,故認為記憶體的invalid read、write的嫌疑最大,就算嫌疑較低,若能排除這個可能性亦有助於除錯,所以決定使用valgrind。
我所使用的指令

valgrind --error-limit=no --tool=memcheck --leak-check=full --show-reachable=yes --track-origins=yes --freelist-vol=62914560 ./penpen 2>&1 | tee log

整份報告有3萬行,不過大部份都是memory leakage有關的,其中要留意的實際只有12項報告,很快就找出了相關的:

==4948== Invalid read of size 4
==4948== at 0x48F8850: QGraphicsSceneFindItemBspTreeVisitor::visit(QList*) (qgraphicsscene_bsp.cpp:79)
==4948== by 0x48F6C21: QGraphicsSceneBspTree::climbTree(QGraphicsSceneBspTreeVisitor*, QRectF const&, int) const (qgraphicsscene_bsp.cpp:247)
==4948== by 0x48F6C97: QGraphicsSceneBspTree::climbTree(QGraphicsSceneBspTreeVisitor*, QRectF const&, int) const (qgraphicsscene_bsp.cpp:261)
==4948== by 0x48F6C5A: QGraphicsSceneBspTree::climbTree(QGraphicsSceneBspTreeVisitor*, QRectF const&, int) const (qgraphicsscene_bsp.cpp:252)
==4948== by 0x48F6C97: QGraphicsSceneBspTree::climbTree(QGraphicsSceneBspTreeVisitor*, QRectF const&, int) const (qgraphicsscene_bsp.cpp:261)
==4948== by 0x48F6C5A: QGraphicsSceneBspTree::climbTree(QGraphicsSceneBspTreeVisitor*, QRectF const&, int) const (qgraphicsscene_bsp.cpp:252)
==4948== by 0x48F6C97: QGraphicsSceneBspTree::climbTree(QGraphicsSceneBspTreeVisitor*, QRectF const&, int) const (qgraphicsscene_bsp.cpp:261)
==4948== by 0x48F6E7E: QGraphicsSceneBspTree::items(QRectF const&, bool) const (qgraphicsscene_bsp.cpp:154)
==4948== by 0x48FAA0F: QGraphicsSceneBspTreeIndexPrivate::estimateItems(QRectF const&, Qt::SortOrder, bool) (qgraphicsscenebsptreeindex.cpp:387)
==4948== by 0x48FAC01: QGraphicsSceneBspTreeIndex::estimateTopLevelItems(QRectF const&, Qt::SortOrder) const (qgraphicsscenebsptreeindex.cpp:540)
==4948== by 0x48E9FD7: QGraphicsScenePrivate::drawItems(QPainter*, QTransform const*, QRegion*, QWidget*) (qgraphicsscene.cpp:4617)
==4948== by 0x490AC9D: QGraphicsView::paintEvent(QPaintEvent*) (qgraphicsview.cpp:3393)
==4948== Address 0x8015be4 is 4 bytes inside a block of size 52 free'd
==4948== at 0x402387B: operator delete(void*) (vg_replace_malloc.c:387)
==4948== by 0x8064A05: GraphicsStroke::~GraphicsStroke() (graphicsstroke.h:10)
==4948== by 0x806773A: SketchPaper::clear() (paper.cpp:107)
==4948== by 0x80577E3: StandardPage::clear() (page.cpp:88)
==4948== by 0x8057BF0: StandardPage::load(int) (page.cpp:33)
==4948== by 0x806A6F5: PageEditorWindow::open(int) (pageeditorwindow.cpp:83)
==4948== by 0x80538FC: Maemo5Application::openPage(int) (maemo5application.cpp:100)
==4948== by 0x807179D: Maemo5Application::qt_metacall(QMetaObject::Call, int, void**) (moc_maemo5application.cpp:79)
==4948== by 0x4D1EADA: QMetaObject::metacall(QObject*, QMetaObject::Call, int, void**) (qmetaobject.cpp:237)
==4948== by 0x4D2C5A6: QMetaObject::activate(QObject*, QMetaObject const*, int, void**) (qobject.cpp:3285)
==4948== by 0x8070B6F: PageViewerWindow::requestOpenPage(int) (moc_pageviewerwindow.cpp:101)
==4948== by 0x80688C6: PageViewerWindow::on_listView_activated(QModelIndex) (pageviewerwindow.cpp:65)

4948是指process的PID,在排除call stack以後,實際的錯誤只有二項:

  1. Invalid read of size 4
  2. Address 0x8015be4 is 4 bytes inside a block of size 52 free'd
第一項是指valgrind認為這裏存取了不當的記憶體地址,這種訊息是最常發生的,而大部份都是誤判,而這裏可以排除這個可能性,因為與gdb的資訊一致。

另外留意一件事,這裏雖然說size 4,但實際所讀取的量不是這個數字也會當成是4,這與memory alignment有關,理由不多說,總之不要深究是否真的讀了4 bytes,也不要因此判斷是否正確。

至於第2項,相信會令人很費解,首先我們要知道,2)其實是在說1)的成因,QGraphicsSceneFindItemBspTreeVisitor::visit錯誤地讀取了記憶體,而這段記憶體在2)的時候發生了一件事。

看一看 2)的call stack:

==4948== Address 0x8015be4 is 4 bytes inside a block of size 52 free'd
==4948== at 0x402387B: operator delete(void*) (vg_replace_malloc.c:387)
==4948== by 0x8064A05: GraphicsStroke::~GraphicsStroke() (graphicsstroke.h:10)
==4948== by 0x806773A: SketchPaper::clear() (paper.cpp:107)

這段訊息要這樣理解

  • a) QGraphicsSceneFindItemBspTreeVisitor::visit嘗試存取0x8015be4這個地址,但這個地址是不能讀取的,正是segfault的原兇
  • b) 0x8015be4這個地址是曾經正確的,那時候有一塊連續52 bytes的記憶體是可以讀寫的,而0x8015be4就在這段記憶體中
  • c) Call stack告訢了你,這段記憶體是在什麼時候被釋放的,那是在paper.cpp的107行發生的。
換言之,0x8015be4是一個dangling pointer,指向了一個已被釋放的空間 - 你就是真兇!(若有什麼說話是最想在現實裹說一次的,相信這是no.1的首選)

那應該很簡單解決的。

但事實上卻不是那回事……因為QGraphicsSceneFindItemBspTreeVisitor::visit()不是由PenPen直接呼叫的,是處理user interaction時被叫喚的,那個GraphicsStroke物件為什麼在paper.cpp:107被釋放後,Qt仍保留了一個 reference呢?

到了這階段就不能靠valgrind幫忙了,valgrind己經成功地排除了我的代碼中有直接做出dangling pointer的可能,餘下的只能翻Qt的說明,再Google了數十遍文章,總算知道原來是Qt的bug...

147478

Crash in QGraphicsSceneBspTree when adding items in response to a mouse press and adjusting them when moving.

Resolution: Always call prepareGeometryChange() before changing the item's geometry. Otherwise, the scene's index will fall out of sync, and the behavior is undefined.

如果prepareGeometryChange()這個function不是依某個次序喚叫,就會令Indexing有*可能*出錯(不一定發生),結果它保留了一份己釋放的reference(dangling pointer),換言之最初的診斷1)及2)是正確的…… 囧

在Refactor前沒有問題,僅僅是因為好彩沒有撞上這個問題…… 後來把prepareGeometryChange()的次序改一改就讓問題解決了。

結論

我認為這個bug的除錯難度屬於中等偏高,在除錯前要先理解到底是自己的代碼導至segfault、還是由library簡接造成,前者只要熟用gdb/valgrind便能快速地驗證,不過熟用valgrind本身就要一定的功力了。

到發現是Qt的bug,若非在google上找到相關的文章,就必需花費大量時間去驗證,要對Qt很熟識才有可能做到。我找到其他類似的bug report,本身要reproduce這個bug已經要花了很多時間(遞交者說要先要弄20k個instance... Orz),而且TrollTech至今仍未解決,要自己弄出解決方法並不輕鬆。
Creative Commons License
本網誌Ben Lau製作,以共享創意署名-非商業性-相同方式共享 3.0 香港 授權條款釋出。