[好文]將LPC程式最佳化

看板mud (網路地下城/文字遊戲)作者 (2008 Fighter!)時間16年前 (2010/01/05 15:52), 編輯推噓0(001)
留言1則, 1人參與, 最新討論串1/1
原始連結 http://bbs.mudbuilder.com/read.php?tid=6205&fpage=2 版主覺得好可以收精華區 我覺得這篇十分重點啊 將 LPC 程式最佳化【好文回顧】 (MudOS 0.9.x 版) 作者: Luke Mewburn 版本: 1.2 日期: 930321 翻譯: Devianth , elon@eastern.stories (10/29/94) 0. 簡介 我寫這篇文章的原因是我注意到 LPmud 越來越傾向複雜化及玩家驅動的功能. 這是好現象, 但因為 LPC 運作的方法 (一次只解譯一部份的程式) 寫不好的程 式碼不但會使的整個 mud 慢下來, 也會使在 mud 上的玩家感到 'lag'. 因為如此, 我決定去找出方法將一些常用的工作寫成有效率的程式並記錄下來 以供參考. 我假設讀者已有一些 C 或 LPC 的程式經驗. 但是, 如果你是新手, 我依然建議你將本文讀完因才能在一開始就學到正確的程式寫法. 雖然我的研究是關於 MudOS 0.9 的 driver, 但許多經驗還是能在其它 LP v3 型的 driver 下使用. 但這並不保證這裡所講的在非 MudOS driver 下是完全 正確的, (比方說 Amylaar 的 3.2@22 driver), 因為各 driver 程式碼已經 有相當大的差異了. 以下的程式碼, 我們將假設這些定義: int i, j, max, bitfield; object *list; string *arr, name; LPC, 像 C 一樣, 有支援 '動態配置'. 也就是 driver 只有在需要 時才會將記憶體分給要用的運算 (完成後會收回). 跟 C 不同的是, LPC 會自動 地在你使用完後收回分配的記憶體 (i.e. 當沒有 LPC 程試碼在使用時) - 又叫 '清除收集'. 字串 (string), 陣列 (array) 和 mapping 都是'動態'處理的. 雖然 '動態配置' 在結構化程式裡非常有用, 但相對的它也使用了非 常多的 CPU 時間. 本文的目地之一就是要教你如何正確的運用 '動態配置' 的好處而不要浪費了這項功能. 1. GENERAL POINTS 在我開始教你如何始某段程式碼加速運作前, 我提出以下一些關於電腦程式寫作 的公理: 第一條, '電腦花 80% 的時間執行 20% 的程式' ('80% of the time spent executing an algorithm is in 20% of the code'), 或是所謂的 '80/20' 法 則. (有些說法是 75/25 或 90/10). 這個經證實的事實可以有效的幫助你寫 LPC 不要花太多時間試著將你所有的 LPC 程試碼最佳化, 你不會有那麼多的時 間. 將精神放在一些較常用, 執行次數較多的區段, 如 for-loops 等. 第二是, 如何選用正確的 algorithm. 大多數的時間, 當一個簡單的 algorithm 就足夠時, 人們會選擇一個複雜 algorithm, 特別是當程式處理少數資料時, 簡單 的 algorithm 通常比較有效率. 要能在簡化及效率上作一番取捨. 因為大部份的 driver 都沒有對 LPC 碼作最佳化的功能 (比方說重寫 expression 或迴圈 strength-reduction), 你能幫的忙就是有智慧的寫自己的程式碼. 一個加速的方法是將一個共用的常數/函式移到迴圈或迴圈測試之外, 並使用一 個暫時的變數. 最長見的就是在 while/for condition (q.v) 時使用 'sizeof(arr)' 在其它狀況下, 有可能某個函式每次都會在迴圈裡被呼叫, 但那個函式每次都會 傳回一個相同的值 - 如果真是這樣, 就在迴圈以外將這個值設定成一個變數, 並 在迴圈內使用這個變數. 例如: for ( i = 0; i < max; i++) if ( list == some_condition ) do_something_with( list, this_player()->query("name") ); 假設 "name" 是不變的, 則以下的方法會比較好: name = this_player()->query("name"); for ( i = 0; i < max; i++) if ( list == some_condition ) do_something_with( list, name ); 雖然說好的程式格式並不會真的加快編譯的速度, 但一個簡單好讀的程式比較 好修改及 debug. 大多數的人對程式的格式有不同的看法, 所以我在這裡也不 會強迫讀者使用我的格式, 我只想告訴你, 選擇一個你喜歡的格式, 然後保持 下去. 2. 資料型態 (DATA TYPES) LPC 至少有以下的資料型態: int, string, object, mixed, mapping. 由以上所組成的陣列也是可能的. 以下是使用這些資料型態的要則: 2.1 整數 (INT) 整數資料型態使用起來非常的有效率, 因為 integer 不需要動態配置. 使用 bitfields (e.g. hand-coded with #defines, (模擬 ANSI C 的 enum's)), 你可以儲存多種的 flags 資料. 這樣會比將資料存成 bit size 來的快, 因為 bitfields 只需要 32 bits. 以下是處理 integer bit fields 的範例程式: (假設 VALUE 為 1,2,4,8, etc.) bitfield != VALUE; // set bit VALUE bitfield &= ~VALUE; // reset bit VALUE bitfield & VALUE // test bit VALUE 如果你要設 bit n, 將 VALUE 取代為 (1 >< n) 2.2 字串 (STRINGS) 字串是最常用的資料型態, 但它需要 '動態配置'. 常數字串(constant string) (i.e, 單純的 'write' 敘述) 是存放於一個 '字串表' 裡, 所以如果要有多重的 copy 通常不需要多餘的記憶體. 字串的加法 (使用 + 符號) 就是一個非常耗時的 運作 - 每加一段字就要配置記憶體一次. 通常我們會這樣寫: write("this is a forest\n" + "and is really boring.\n" + "You are feeling sleepy\n"); 這是最糟的方法來寫一個常數字串 - 當這個 object (物件) 被編譯時會非常的慢, 而且也沒有有效率的運用記憶體. 值得注意的是, 在 MudOS 0.9.15 [我們用 0.9.19.x] 在字串加法裡不一定要用 '+' 這個符號來代表 - 但這樣只是節省了原 始程式的大小 - 它還是需要使用動態記憶體分配來使用每一段在 " " 裡的字串. 試試以下: write("\ this is a forect\n\ and is really boring.\n\ You are feeling sleepy.\n\ "); 這樣的話所有的文字都存在一起. 你通常能用這種方法存取一頁以上的文字, 超過 這個數量時你的 parser 可能會嗆到, 試著將你的文字分段, 並使用分開的 write statement. 注: 注意, \ 後到一行的結尾(eotl)之間不能有空白字元, 否則 parser 會抱怨. UPDATE: MudOS 0.9.14.3 以後, 指定常數字串時有一個新的符號 - '@', 所以上 述範例也可以寫成: write( @ENDMESSAGE This is a forect and is really boring You are feeling sleepy. ENDMESSAGE ); ('ENDMESSAGE' 是用來作為分界點的, 可以是任何不在 main body(?) 的任何字串, 而且它一定要在一行的最開端使用, 要不然它不會被當做 '訊息結束' 的記號.) 另一個沒有效率的方法: write("Your name is " + this_player()->query("name") + "\n" + "and your level is " + this_player()->query("level") + "\n"); 如果你的 driver 有 printf(), 試著用以下的方法: printf(Your name is %s\nand your level is %d\n", this_player()->query("name"), this_player()->query("level") ); 其它還有很多類似的例子, so you get the general idea. 但不要誤解我的意思, 字串加法是很常用的, 所以不要試著去用一些很長的運作或 技巧來避免用到字串加法. (因為這通常會比使用字串加法還慢). 2.3 浮點 (FLOATS) 0.9.15 以後的 driver 都有支援浮點(floating point)資料型態. 在宣告變數時用 'float' 這個 keyword. (對寫 C 程式的人來講, 這和 C 的 'double' type 相同) 像 cos, sin, (和類似的運算), ln, 和 sqrt (平方根) 等的的運算函數也可以使 用. 這種資料型態可以在編輯時暫時跳過, 但我看不出來這有什麼需要. 浮點的運算比整數慢、所以能用整數運算時盡量不要用浮點運算。需要用 到浮點運算時, 盡量先把它們算出來以迴避重複的運算(這點對於任何程式都通). 2.4. 陣列 (ARRAY) 陣列是非常有用的, 但跟字串一樣, 不當的使用會造成低效率的程式. 一個在迴圈內有增加或除掉項目的迴圈通常是較慢的. 解決的方法是適當的運用 記憶體預先配置。 摘自 TMI-2 的 /adm/daemons/cmd_d.c : bin_ls = get_dir(path + "/"); result = ({ }); for (i = 0; i < sizeof(bin_ls); i++) { if(sscanf(bin_ls, "_%s.c", tmp) == 1) result += ({ tmp }); } cmd_table[path]=result; 這裡, 並不是所有的項目都有選到, 被選到的也是在被修改過後再放到最後 的陣列中. 以下面的方法取代: bin_ls = get_dir(path + "/"); i = sizeof(bin_ls); result = allocate(i); j = 0; while (i--) { // 使用 'while' 的原因請參考下面 if(sscanf(bin_ls, "_%s.c", tmp) == 1) result[j++] = tmp; } cmd_table[path]=result[0..j-1]; 因為我們知道結果一定是 <= sizeof(bin_ls), 我們就只配置這麼多空間. 然 後再依需求增加項目. 最後, 在我們們使用結果前, 將陣列調到正確的大小. (使用 [0..j-1] 運作). [這裡的英文原文少了一段, 整節看來沒有意義, 故省略] 預先配置最大的優點就是在用預先配置時, 處理時間和項目數目呈線性正比 (linear) 而用 += 的陣列處理和處理時間成指數比增加 (exponential). 2.5. MAPPINGS Mappings 就是連接性的陣列 - 能用基本資料型態(int, string, 或 object) 來做索引的資料結構。以資料結構來說, 它通常比陣列來的有效率, 並可以用來 模擬 C 裡的 'structs' 敘述. 基於 mappings 內部儲存的方式, 預先配置的 mapping 比較有效率. 如果你已經知道 mapping 裡的元素會保持在 x 個 (還可以再增加), 而且不會因 為使用 map_delete 而使總元素低於 x 個, 則此法特別有效率. (比方說, 當一個 mapping 已預先配置了 x 個元素, 在大多數的元素都被用 map_delete 刪除後, 你將會浪費很多記憶體因為 map_delete 將不會把預先配置的 mapping 裡沒有用 的記憶體釋放出來.) 一個常見的預先配置方法可在 emote daemon 或像 /std/user (或 /obj/player) 裡的標準物件中找到. 對於前者, 如果你已經有 200 個左右的項目, 不要用下列方法來格式你的 mapping: emotemap = ([ ]); 應該改用: emotemap = allocate_mapping( 200 ); 3. 控制程序 (CONTROL FLOW) 以及 迴圈 (LOOPING) 在一個完整的 LPC 程式中, 控制程式執行的程序 (由測試, 以及部份的迴圈所 組成) 可以因為正確的使用不同的形式而變得比較有效率. 3.1. WHILE 最簡單的 (也是在簡化型裡最快的) 迴圈使用方式. 常見的是: list = users(); for ( i = 0 ; i < sizeof(list) ; i++ ) do_something_with_item( list ); 這是非常的沒有效率, 因為 sizeof(list) 的值每次都要被重新計算出來 (參考 上面的 GENERAL POINTS 一節). 如果你想將整個 list 反過來排列, 試著使用以 下的方法: max = sizeof(list); // slight performance gain i = -1; while ( ++i < max ) //evaluate and increment at same time do_something_with_item(list); 如果順序沒有什麼太大的關係, 以下是最快的方式: i = sizeof(list); while (i--) do_something_with_item(list); 這就是 '簡單的調件 while 迴圈' ('simple condition while loop'). 和下列的 實在是沒什麼太大的不同. for ( i = sizeof(list) ; i-- ; ) ; 以及相對等的 while 迴圈. 但還是有少許利益可得. 3.2. FOR 最常見的迴圈結構之一, 這在當你每次都需要在迴圈結述前執行某個比較複雜的 動作時非常有用. 如果只是使用簡單的指數增加或減少, 用 'while' 敘述會比較 好. 如果 '結述運作' (ending operation) 比較複雜, 則以下的通用例子: for ( initialise ; test ; final ) { main body } 比下列清楚: initialize while ( test ) { main body final } 3.3. 開關 (SWITCH) switch 敘述應該盡可能的用來取代 'if-then-else' 型結構. 因為新的 driver 都有對 switch 敘述作相當程度的簡化. 另一方面, 用 switch 敘述的程試碼看來 也比較 '乾淨'. 用 switch 敘數的另一個優點是 `狀況範圍' (case range) 的支援. (一個 C 沒有 的特色). 如果你的測試設定如下面的範圍: case 1: case 2: case 3: 可用以下代替: case 1..3: 可能比較有效率 (至少字少打一些) 4. 結論 我確定這份文件中一定有錯誤, 但是人不是完美的. 以下的人對本文的 1.1 版提出問題, 意見: @TMI-2: Blackthorn, Mobydick, Square, Alexus, Amylaar, Darin @Ivory.tower: Telsin, Vampyr @Underdark: Cynic, Brian Luke [Zak]. 程式的最佳化 ============== 在optimizing_code裡已經談到了一些最佳化的簡單注意事項。然而最 佳化並不是少數adm或大巫師的事,而是每一個巫師寫程式都必須注意 的事項。因此請大家在完成一個作品時,再花一些時間重看一下程式 ,看看可不可以 藉由一些簡單的更改來避免一些不必要的計算,即使 只是節省一兩個計算也是好的。當然也不必為了省一點點的計算而花 很多時間或是把程式寫得很複雜。但是一些簡單的原則,或是稍微注 意一下,就可能 會有很大的影響。 我們來看一個簡單的例子,底下是/adm/daemons/aim_d.c其中的一段 程式,注意看第七行: if( random(100) < 30 && !me->query("npc") ) return 0; 本來放在倒數第五行,也就是變成註解的地方。我看到了,就把它移 到現在的位置。這樣有什麼差別呢?如果在原來的位置,中間作了一 堆關於skill的計算,當此行成立,這些計算都浪費了。而移到第七行 的位置,當此行成立,就不會去計算skill,只是更改一下程式的位置 ,就可以節省許多不必要的計算。這個程式是醫生每回合攻擊時都會 呼叫到,你可以想像原來的寫法作了多少不必要的計算。 int aim_target(object me, object victim) { int skill, diffculty; string loc; object weapon; // difficulty for player if( random(100) < 30 && !me->query("npc") ) return 0; skill = (int)me->query_skill("anatomlogy"); loc = (string)me->query("aiming_loc"); if( !skill || !loc ) return 0; diffculty = diffs[loc]; if( undefinedp(diffculty) ) return 0; diffculty += diffculty*(int)victim->query("aim_difficulty/"+loc)/100; [中間省略] skill /= 10; skill += (int)me->query_stat("int") * 2 + (int)me->query_stat("kar"); skill -= (int)victim->query_stat("int") * 2 + (int)victim->query_stat("kar"); // if( random(100) < 30 && !me->query("npc") ) return 0; if( random(skill) < diffculty ) return 0; return (int)call_other( this_object(), "hit_" + loc, me, victim ); } 在考慮程式最佳化時,先想想這段程式被使用的頻率有多高,如果是 使用頻率很高的程式,那就要多花些心力注意最佳化。一般使用頻率 較高的程式大略是/adm/, /cmds/, /std下的程式,不過這不是一般 巫師可以動的,另外公會的一些程式,武器的特攻、NPC的tactic,以 及一些init,relay_message的程式等等。這些都是使用頻率很高的程式 -- ※ 發信站: 批踢踢實業坊(ptt.cc) ◆ From: 114.42.238.20

01/05 16:01, , 1F
對了 這篇是傳說中的elon翻的嗎?看mail好像是耶(拜)
01/05 16:01, 1F
文章代碼(AID): #1BGk_6XA (mud)
文章代碼(AID): #1BGk_6XA (mud)