This page looks plain and unstyled because you're using a non-standard compliant browser. To see it in its best form, please visit upgrade to a browser that supports web standards. It's free and painless.

Fillano's Learning Notes 會員登入 會員註冊

為了練習,自己寫了csv -> xml(office 2003)的轉碼程式,中途跌倒了很多次,主要原因都是自訂的node類別。

原先的簡單想法是,每個node都會有一個render方法,在這個render方法裡面遞迴地呼叫子node的render方法,把整個文件產生出來。寫法很簡單,結構很漂亮,但是.....跑得好慢。

拿來測試的csv有1200筆資料,每一筆資料有五個欄位。每個欄位用一個textnode來代表,然後用data跟cell node包起來,每筆資料用row node包起來,外面在加上table,worksheet,workbook三個node。估計一下,這樣得要在系統裡面產生3*5*1200+1200+3,19203個node物件。

估計一下,有幾個重要的瓶頸:

  1. 因為用遞迴的方式呼叫render方法,過程中會遍歷所有的節點,所以.....這些node物件佔用的記憶體都不會被釋放,直到根節點的render方法執行完畢。
  2. 跑完所有的render後,才產生完整的檔案內容字串,然後寫入檔案。只寫了一次,所以即使使用BufferedWriter也不能增加效率。
  3. 集中釋放這19203個物件的Garbage Collection動作,非常花時間。

看起來只要解決兩個問題,就可以大幅提高效率:

  1. 分批產生node物件然後釋放,避免一次釋放。
  2. 分批寫入檔案,使用BufferedWriter提高寫入的效率。

我想到的方法有兩個部份:

  1. 把render拆成三個方法,就是renderHead負責開始的tag,renderFoot負責結束的tag。呼叫子節點的render方法部份拆出成renderBody,但是並不實際呼叫。然後在render方法裡面依序呼叫這三個,測試看可行否。
  2. 分析了一下xml的文件結構,看起來可以用stack來處理這個結構,每個節點在進入子節點前,先呼叫renderHead,然後把自己push進堆疊。接著子節點也依照父節點的步驟順序執行。所有子節點訪問完了,pop出來執行renderFoot,然後用null釋放。

經過這兩個步驟,文件產生的過程已經變成循序的了,所以就直接把renderHead與renderFoot的結果直接寫進BufferedWriter。

跑了一下:恩.....速度快了非常多,應該有三十倍左用吧?記憶體也用的非常少,大概只有十分之一。(在main裡面用System.currentTimeMillis()來評估開始結束的時間差,用-verbosegc選項觀察heap使用的峰值來評估記憶體使用)

感想:遞迴的結構是很漂亮,但是執行起來效率實在是.....


2007-11-20 7:56 AM 修改

記憶體耗用的比較,沒有扣掉空的main似乎不夠準確。我另外寫了一個空的main,裡面用System.gc()強制做GC,然後用-verbosegc選項來跑,紀錄gc時的heap峰值。上面記憶體使用如果扣掉空的main,差距可以到18倍左右。


2007-11-20 8:59 AM 修改

嗚嗚,看錯欄位了,記憶體耗用沒差那麼多,大概是六到七倍。不過這樣估計應該不是很準,用遞迴的方式node建立完時,用到的記憶體大概是render結束時的大約1/2。不過產生的檔案很大,有584KB。這也用了不少記憶體。


2007-11-22 9:10 AM 修改

仔細考慮的一下我用的方法,其實要把csv的欄位填入xml時,xml的架構都沒有改變。所以其實可以先產生各個節點,然後再依照順序呼叫各個節點的renderHead、renderFoot方法,這樣可以重複利用節點物件,根本不用再重複產生,也不必用stack結構來存放。

最近試做了一個csv/xml雙向轉檔的jscript(用到WSH)。為了提高程式的效率,所以嘗試了幾種方法,發現透過陣列的元素,或是物件的屬性,是可以達到call by reference的目的,但是直接把物件傳入函數,對物件本身的一些操作是沒有效果的。

以下是沒有效果的例子:

var a = "a test";
alert(a);
test(a);
alert(a);
function test(str) {
	str += " more";
	alert(str);
}

執行上例會依序跳出"a test"、"a test more"、"a test"三個alert對話框,在執行完test函數後,變數a(String物件的instance)並沒有改變。

但是不要以為完全做不到call by reference的效果,其實利用物件屬性或是陣列,可以達到目的。以下是物件屬性的例子:

var a = {"b":"a test"};
alert(a.b);
test(a);
alert(a.b);
function test(obj) {
	obj.b += " more";
	alert(obj.b);
}

執行上例,會依序跳出"a test"、"a test more"、"a test more"三個alert對話框,可見傳入的物件屬性真的被改變了,可以達到call by reference的效果。

使用陣列的例子:

var a = ["a test"];
alert(a[0]);
test(a);
alert(a[0]);
function test (arr) {
	arr[0] += " more";
	alert(arr[0]);
}

另外,使用物件的方法對物件進行操作,對於用call by reference是否有效呢?試試看:

var a = [];
alert(a.length);
test(a);
alert(a.length);
function test(arr) {
	arr.push("a test");
}

在test函數中對傳入的陣列用push方法增加元素,可以成功進行。執行上例會跳出"0"、"1"兩個對話框,表示陣列元素增加了。

另一個可以操作的例子是Date物件:

var a = new Date();
alert(a.getTime());
test(a);
alert(a.getTime());
function test(date) {
	date.setTime((new Date()).getTime());
}

透過setTime方法,在test函數就可以改變傳入的Date物件代表的時間。

使用者自訂的物件也有類似的效果:

var a1 = new a();
alert(a1.b);
test(a1);
alert(a1.b);
function a () {
	this.b = "a test";
	this.appendB = function (str) {
		this.b += str;
	}
}
function test (obj) {
	obj.appendB(" more");
}

總結一下。傳入物件給函數操作時,如果操作的對象是物件本身(的primitive value?),不會有call by reference的效果,但是透過物件的方法或屬性來操作,則有call by reference效果。

(至於轉檔程式.....其實算是失敗,倒不是不能跑,而是用自己寫的node物件來產生xml,我要轉的檔案會產生兩萬個樹狀的nodes,處理起來實在太慢,只好改成用直接產生xml內容的方式做,但是這樣就不能因應xml結構的改變。也許改用flyweight來寫會好一點,有空再來試試。)

只是簡單的感想。算是()的妙用吧。

之前就知道用()來做出匿名函數,其實他還有更多匿名的作用。例如:

如果我要用Date物件來取開始時間與結束時間,計算開始到結束需要花費的時間,通常直覺的方法是:

var date1 = new Date();
var startTime = date1.getTime();
..........
..........
..........
var date2 = new Date();
var endTime = date2.getTime();
var duration = Math.round((endTime-startTime)/1000);

產生Date物件只是為了取得目前的timestamp,特別assign一個變數給它,似乎有一點浪費,這時候就可以用()做到匿名的效果:

var startTime = (new Date()).getTime();
..........
..........
..........
var endTime = (new Date()).getTime();
var duration = Math.round((endTime-startTime)/1000);

這樣可以少兩個變數,也少打幾個字。哈哈。

想確認一下()到底精確意義上在做什麼,不過ECMA-262還真是難以查閱......好不容易找到了他對於()的解釋:

11.1.6 The Grouping Operator
The production PrimaryExpression : ( Expression ) is evaluated as follows:
1. Evaluate Expression. This may be of type Reference.
2. Return Result(1).
NOTE This algorithm does not apply GetValue to Result(1). The principal motivation for this is so that operators such as
delete and typeof may be applied to parenthesised expressions.

嗯嗯,看起來(expression)會把括號當中的敘述grouping起來先執行,然後傳回執行的結果。不知道執行複雜的敘述會發生什麼事情呢?

.......嗚嗚,做不出可以放進()的複雜敘述,Return的意思並不是真的return什麼,而可以說是這一段敘述產生了什麼物件。例如:

alert(("this is a test").length);//就地生成一個字串物件,顯示他的長度
alert(("a"==="a").toString());//就地生成一個Boolean物件,顯示他toString函數執行的結果。

用間接的方法,可以看出()的效果:

alert(test().getTime());
function test() {
	return new Date();
}

test函數會返回一個Date物件,然後我們可以直接對他操作。用()可以讓這個過程更簡單。


2007-11-18 修改

稍微複雜一點,可以用asign(=)跟三元運算子(?:)做出來。例如:

var b=0;
var a="The result is: ";
alert((a+=(b>0)? "yes":"no").length);
alert(a);

簡單地說吧,看起來雖然ECMA-262定義中限制不多,只說是(expression),但是這個expression是要可以合法地放在assignment運算子右邊的敘述。可以加上assignment運算子跟左側的identifier,但是不可以在identifier之前加var關鍵字.......所以可以放在grouping operator中的就是ECMA-262定義的AssignmentExpression囉?(java的用法好像差不多)