pretty code

2024年6月12日 星期三

關於 Verilog parser 這件事

連假都在盡最後努力讓自己的聖騎升上 97 級,再加上手上沒有那樣大的 netlist 檔案,所以放假三天就在打電動中渡過了XD

昨天一早就用 cProfile 確定最慢的執行函數是什麼?果不其然,前三名是 PLY yacc.py - parseopt_notrack、 PLY lex.py - token 以及 re.match,第四名 pyverilog parser.py - p_items 看起來貌似 constructor 沒錯?


光前三個就跑了 25 分鐘以上(cProfile 會增加程式執行時間,跑起來要更有耐心)。

要細究是哪個段落花時間只能 print message 了,但我想幫助不大,因為 PLY 應該已經是 Python 最有名的 lex/yacc solution 了,要優化應該也不是一天兩天的事。

順便實測其他 EDA tool 的花費時間,原來也不是我想的秒等級!

雖然這樣比有點不公平,比如 DC 還需要知道 cell library,還有不論是 DC or yosys,貌似 read_verilog 不只是 parsing 都還順便建立了 RTLIL 中介層,應該是比 pyverilog 只用一般資料結構儲存花時間,但也只能這樣比較了,畢竟我已經無法再細拆 command 了。

同樣的 netlist 檔案,花費秒數如下:

DC - 191
yosys - 380
pyverilog (建完語法樹) - 1546,離開程式 Python 釋放記憶體又花了至少 100 秒。

嗯,果然還是有一段差距。

2024/06/18 更新

透過 cProfile 分析得知,parser 花了很多時間做 match,撇開 C vs Python 語言差異不談,最快的解決方式就是只要處理 netlist 語法即可,不需要處理整個 Verilog 語法。

稍微查了一下,只有看到 C 語言版本的 netlist parser,再加上看 EDA 文件真的很 borning(EDA 文件都要真的操作才有感覺,還沒碰過的光看文件,坦白說收益不大),所以還是自己來吧。

從上星期五開始,扣掉家裡有事請假外,總共花了 16.5 小時使用 Python 實作。

概念也是非常簡單,我有一個函數負責取得以分號為結尾的字串或是 endmodule 關鍵字,這裡我稱呼它為工作字串,再來就是對工作字串做比對,只要分別針對 module,port,net,cell instance 處理並儲存結果即可。

目前暫時還沒想到 assign 要儲存啥以及偷懶不想處理一維陣列以上的情況,其餘我知道的語法都已處理完畢。

開發時間分析如下:

得到可以工作的字串 - 2.5 小時
使用 Rex 處理所有語法 - 8 小時
將儲存的資料結構轉成 pyverilog 的輸出格式 - 6 小時

程式碼行數:

得到工作字串 - 89 行
輸出 Verilog - 149 行
Rex 處理及其他 - 455 行

程式執行時間:

單純 parsing 及儲存 - 225 秒
輸出 Verilog - 56 秒

下面則是我的 netlist parser cProfile 結果:


目前還算滿意XD

雖然從結果來看,還是有進一步優化的可能性,但我覺得最多不可能再減少超過 50 秒?

就算再減少 50 秒也沒意義!我只有處理 netlist,不像 EDA Tool 處理整個 Verilog 語法,這樣的 parser 跟商用軟體比還是有段距離,但貌似我的工作大部分應該還是針對 netlist 做事?

個人還是覺得 lex/yacc 的方法比較正統,不像我的 code 為了解決縮排,使用者 Code 亂以及空白字元等問題,需要一些 if/else 來處理(取工作字串 89 行程式碼中,大部分都是在處理這些事情),這樣的 code 看起來也不開心XD

2024/06/21 更新

前天進公司後,想了一下還是加上多維陣列的功能,大概花了不到 5 分鐘吧。

沒想到這兩天拿它來試試其他手上有的 netlist,陸續又發現了一些問題,也順便修了 pin net 中,有 range 及 array 情況下,Rex 沒下好導致 capture group 抓取不正確的問題,昨天下午看了 Verilog 2001 Spec 後還發現我搞錯順序了,應該是先陣列才是 range,目前應該都沒問題了吧?

為了這個多維陣列,code 多了 30 幾行,程式執行時間也多了 20 幾秒,目前 parsing 大概要跑 245 秒左右。

單純 parsing


單純 parsing 加上處理多維陣列結果


最後想嘗試一下把我這個 parser 用到之前拔電路的小程式中,一般來說,我有兩個選擇:

1. 將我目前用的簡單資料結構轉成 pyverilog ast 結構再加上個別 class visit function,這樣我不用去改原本的 code。

2. 配合我 parser 用的簡單資料結構改寫我原本的 code。

目前是嘗試使用方法二,今天應該就能知道結果了。

對了,這個 600 多萬行的 Verilog,我的 parser 執行時會用掉 10G 多一點的記憶體,離開好像也是要花個幾秒等記憶體釋放。

跟之前用 pyverilog 的小程式比較,大概快了 35 分鐘,也不枉費我這幾天的努力XD


我的程式總共建了 9 條 Rex。

因為我的程式把行打散了,故我無法得知行數資訊,先前簡單用個 group 概念好讓自己的輸出接近 pyverilog 格式。

之前是用 yosys read_verilog + write_verilog 方式來驗證我的 parser 正確性,我們可以用 design -reset command 將兩個要比較的檔案寫在同一個 script 讓 yosys 幫忙轉檔,同一個檔案,讀加寫要花 30 分鐘(沒記錯的話,寫檔就花了 20 幾分鐘)。

由於之前就知道要怎麼改,故最後還是把 module.port_list 再包一層,裡面儲存另一個 list,這樣我就能讓同 group 的在同一行輸出,加上這個功能之後,我的輸出基本上就跟 pyverilog 一致,除了多維陣列還未驗證過 pyverilog 格式長怎樣。

也因為這樣輸出 verilog 函數行數再度減少,最終版本 parser 包含測試 main 函數只有 701 行(先前的行數統計也是包含 main)。

目前只剩一個謎題待解?

我能理解 pyverilog 因為比對緣故所以花了很多時間,但為什麼單純輸出 verilog 也要花上 10 來分鐘,對比我的 60 秒確實是有不小的差距!我猜 pyverilog 應該是使用 template file 方式產生 verilog,故浪費了很多時間在 File IO 。

其實 yosys 寫檔時間比 pyverilog 還更誇張,我能想到的就是它又二次轉了資料結構以利 verilog 輸出?

剛剛看了一下,輸出是在 yosys/backends/verilog/verilog_backend.cc 這支檔案,看起來是用 RTLIL::Design *design 來儲存整個 netlist,並直接用 std::ostream *&f 寫到 output,看來想知道事情緣由還是要把整個程式碼看過一遍才行。

2024/06/23 更新

快速的看了一下 yosys code,backend - write_verilog 呼叫順序為:

yosys/kernel/driver.cc - main 函數入口
中間未看…
yosys/kernel/register.cc - Backend::execute(這個應該沒有,因為被 verilog_backend 繼承了)
yosys/backends/verilog/verilog_backend.cc - execute,這裡會呼叫 Backends::extra_args,裡面會把 
原本是 NULL 的 std::ostream 在這裡開檔,之後便能寫檔了,這也是為什麼 execute 會接受一個 std::ostream *&f 的緣故,因為需要在這裡 new instance。

2024/06/25 更新

yosys 可以吃 tcl script file,故有用到 Tcl 提供的介面,script 裡面要執行的 command 其 callback 在 kernel/yosys.cc - tcl_yosys_cmd。

在 Pass::call 的過程中,便會呼叫到繼承 Pass 的相關 struct。

2024/06/26 更新

沒用 C++ 寫過大型程式,別說你懂 C++!

從以前到現在沒有認真記過 C++ 語法,導致看 yosys code 很吃力!很多執行順序不是現在的我看的出來的!

The virtual function of base class means inheritance class can override it, you could use override to tell compiler the function is overrided.

baes - virtual void function( )
inheritance - void function( ) override {}

The virtual function of base class equals 0 means the base class can't be created. The inheritance class must override this function.

base - virtual void function ( ) = 0
inheritance - void function ( ) override {}

另外,如果 class 有 static function,其 各個 Pass constructor 會在 main 之前就被呼叫,可能要看 C++11 3.6 節,這是我用 gdb 單步執行才發現的(我的理解可能有誤,至少我另外寫支小程式看不到這個現象?)。

kernel/register.cc - struct Pass 有一個成員 next_queued_pass,在 constructor 時會被設定,然後透過 register.cc global variable - first_queued_pass 記住最後一個 constructor 的 Pass。

接著 main 函數中會呼叫 yosys_setup,裡面會再呼叫 Pass::init_register(執行順序會倒著回來,之前的 first_queued_pass 記住的是最後一個 Pass constructor,沒記錯的話是 techlibs/sf2 - SynthSf2Pass)。

Pass::init_register 裡面會再呼叫 Pass::run_gister。

然後就是對 global variable - pass_register 設值,map 用的 key 就是 pass_name。

之後假設是執行 script 裡面的 command,最後就是在 Pass::call 裡面利用 pass_register 去執行該指令(pre_execute、execute、post_execute),這樣又會回到繼承 Pass 的 struct 裡的 execute 函數(backends/verilog/verilog_backend.cc - execute),因為已經被 override 了。


看到這邊對 yosys 怎麼呼叫有感覺了,應該不會再看下去了?

另外,今天針對同一個 netlist 呼叫 yosys read_verilog and write_verilog,真的要花 31 分鐘左右,這樣看來寫檔真的要 20 幾分鐘跑不掉。

接著把 yosys 儲存的 netlist 再用我的 parser 測試,parsing 時間又增加了 20 幾秒,寫 netlist 還是差不多 60 秒左右,總花費時間大概是在 360 秒內吧?

pyverilog 總執行時間則是暴增到 54 多分鐘(3275 seconds)。

其實這也很容易解釋,yosys write_verilog 對每個 wire declaration 都是單獨一行,故增加了 parsing 時間也是尚屬合理。

原本 netlsit - 340M,  618 萬行。
yosys netlist - 390M, 2166 萬行。

2024/06/27 更新

終於知道為什麼繼承 Pass 的 struct 都會在 main 之前 call constructor 了!跟 static member function沒有關係。

yosys 在每個繼承 Pass 的 struct 最後面宣告處宣告了一個 global static variable,所以才會在 main 之前 call constructor。

以工作站的 yosys 版本來說,總共有 235 Pass 被初始化了。

也就是下面圖片 Line 9 做的事。


下面是 GDB 下中斷的結果


我們可以用 grep -E -r '^\}\s*[a-zA-Z0-9]+\s*;' 找出所有這樣宣告的 Pass,這裡以 backends 為例。


2024/07/08 更新


parsing 106 秒
save netlist 22 秒

對比 Python 版

parsing 268 秒
save netlist 60 秒

搞定,收工XD

沒有留言: