為了提升DL模型效能,阿里工程師打造了流式程式設計框架
阿里妹導讀:隨著深度學習在全球的風靡,演算法模型層出不窮,如何將演算法落地到生產環境中成為了熱門研究領域。目前提高演算法執行效率的主要聚焦點為優化模型結構、將權重資料進行量化,圖優化等方面,然而,它們或多或少都會對模型精度帶來一定的損失,不能做到完全的無損優化。
作為工程開發人員我們能否從模型的執行模式上面進行相應的改造優化?流水線操作的優秀表現已經在工業領域得到體現,在不增加演算法開發複雜度的前提下能否把它應用到AI任務排程系統中?基於這些問題,我們的流式程式設計框架應運而生。
一、背景
進入網際網路下半場,人口紅利逐漸削弱,新零售成為獲取線下流量的重要入口之一。阿里搜尋團隊在新零售領域也進行了積極的探索,成立了客流專案,將線下商超進行數字化升級,商家根據消費者意圖指定相關的改進和升級,提升購物體驗。下圖給出了線下門店實際場景的示例資料,從圖中可以看到,實際商超環境比較複雜,人員分佈隨著環境不同也有很大的差異,在複雜環境下需要達到很好的資料提取效果,對深度學習模型的精度要求比較高,模型計算量很大,對硬體資源要求較高。
客流專案的整體軟體工程模組主要組成部分如下圖:
生產環境下,攝像頭解析度基本都是1080*1920以上,攝像頭個數也比較多(某些門店高達30個以上),所部署的都是大計算量高精度的深度AI模型(比如行人檢測,行人重識別和動作識別等網路模型),線下場景所用的機器GPU為GTX1060,這個配置的機器要求每秒處理這麼多資料顯然很有挑戰,我們工程團隊在如何高效進行AI計算方面進行了深入的研究和探索,提出了模型的流水線執行優化方案。通過如下圖可以清楚解釋流水線任務排程的優越性:
上圖採用的五級流水線進行說明,可以清楚看到流水線模式使得同一時間從執行1條指令提升到5條,也就意味處理速度提高了5倍。正由於流水線在效能提升上有驚人的效果,我們將流水線技術引入到深度學習的工程化優化中,將硬體資源得到更大程度的利用,充分發揮CPU/GPU的計算潛能,並取得優秀的結果。也許還有人會疑惑為什麼不採用傳統的併發方法來實現呢?比如總共有三路視訊,分別用三個程式進行處理,每個程式將所有的任務都做完,這樣也能達到併發的效果。之所以不用這種簡單併發方式原因是這樣的:
1、模型任務之間是有相互依賴的,如果用簡單多程式進行併發,執行到中間需要等待其他程式的資料,這樣會造成資源浪費,比如在跨攝像頭跟蹤的時候,需要將所有攝像頭資料進行分析,這就會導致一個程式的任務需要等待其他程式將其他路攝像頭資料處理完之後進行統一排程分析,這種程式間的依賴和相互等待將嚴重降低執行效率。
2、 一臺機器上的資源是有限的,多個程式都會獨佔儲存資源和計算資源,多個程式進行單獨運算意味著需要開啟多個Tensorflow例項,會佔用大量資源,一旦記憶體資源不夠,觸發了虛擬記憶體交換,就會很卡,導致執行效率急劇下降,甚至會出現頻繁crash的情況。
3、計算資源在多個程式之間切換會大大降低計算效率。
4、流水線技術對執行任務進行了切割和重排,使得有限的資源能夠發揮最大的效用。
由於簡單並行具有很多侷限,我們將工程程式碼進行了流水線模式改造,AI任務的流水線執行模式的實時釋出,使專案效能得到了大幅度的提升,從之前的最多隻支援實時處理8路攝像頭資料提高到了16路攝像頭,計算效率提高了一倍,資源利用率也得到了大幅度提升,最大化利用了硬體資源,在相同硬體資源下,能處理的資料量翻倍。
當然這個改造過程也是有成本的,我們在享受流水線技術帶來的計算效率提升的同時,也帶來了程式碼複雜度的指數級增加。在實際的生產過程中人工流水線的主要缺點有:
模型迭代效率低:新增模型功能需要很大的改造成本,基本需要將演算法之前寫的程式碼重構,融入流水線程式碼中;
任務排程與演算法邏輯相互耦合:給人感覺從高階語言進入組合語言,程式碼的晦澀難懂程度可想而知;
靈活性差:對任務的描述能力不強,任務排程不夠靈活,抽象程度低,很難將一些新的優化措施應用上去。
這些缺陷嚴重製約了優化方案的應用,怎麼樣才能既能夠提高計算效率又不影響模型的迭代效率?最好還能讓演算法同學容易上手,在這種情況下任務自動流水線化框架應運而生。
二、人工構造流水線與自動流水線主要對比
人工流水線:指的是需要人工將演算法過程進行相應的分割,並人肉將任務用獨立程式來執行,資料通道需要高度定製化。
自動流水線:指的是所有的任務排程過程,資料通道構建全部自動化完成,只需要把訓練模型時候的程式碼進行移植過來即可,無需關心流水線任務排程所需要的中間過程。
我們分別從演算法邏輯與流水線排程邏輯關聯性、資料管道建立、開發靈活性、穩定性等幾個方面進行了對比,從下面表格看出,在上個版本人工構建流水線的開發體驗是相當的糟糕,各個方面都是完敗自動流水線框架的表現。
人工流水線 | 自動流水線 | |
AI任務與排程邏輯 | 相互耦合,演算法任務與流水線的排程相互耦合,晦澀難懂。 | 相互獨立,工廠模組與演算法模組相互解耦。 |
資料管道 | 需要為每個管道開發一套程式碼,邏輯複雜。 | 只需要描述資料結構,不用關心管道建立。 |
靈活性 | 牽一髮動全身,一個小的排程優化需要改動整個程式碼結構。 | AI任務與工程解耦,只需要關注工廠排程邏輯。 |
穩定性 | 穩定性差,很容易出現邏輯錯誤。 | 穩定性強,容易寫單元測試驗證。 |
易用性 | 很難上手,演算法同學不僅需要開發演算法邏輯還需要開發排程邏輯任務,訓練完模型需要二次開發。 | 容易上手,演算法同學只需要開發演算法邏輯,模型訓練完即可遷移過來使用。 |
三、系統框架介紹
1、設計理念
開發這麼一套任務自動流水線化框架之初,我們對採用哪種程式語言、如何讓演算法熟練簡單高效描述演算法任務進行了多方面的討論和論證,基於這個框架的目標客戶是深度模型的演算法同學,所有的嘗試和優化點都會基於以下兩個理念進行:
以模型訓練完程式碼直接複用為第一要義:模型訓練完之後程式碼可以直接移植到生產環境執行。
以服務演算法同學為第一準則:模型任務描述簡單高效容易上手。
鑑於大部分深度學習的演算法同學在訓練模型的時候都是基於Python語言編碼,他們使用的訓練框架是大眾化的Tensorflow,經過與演算法同學探討後,我們果斷選用Python語言進行開發,可以更大程度提高演算法模型的迭代效率,對AI任務的描述也是參考Tensorflow構建模型的過程,這樣可以大大簡化演算法同學的開發難度和學習成本,容易上手。
2、系統啟動流程
這裡借用了Tensorflow對模型的強大描述能力,參考Tensorflow框架的的構建和執行流程,自動流水線框架的啟動執行流程如下:
從框架的執行流程圖可以很直觀的看出,主要分為以下三個階段:任務描述、構建任務關係圖、圖執行,這些過程都能夠類比到Tensorflow的啟動過程,從框架執行上,會使用Tensorflow的演算法同學肯定會很方便就能上手使用自動流水線任務排程框架。
3、主要模組
根據工廠任務排程和監控的完備性,我們對流水線執行時期的各個階段進行了高度的抽象,其中各過程模組跟實際工廠車間執行機制高度統一,方便框架的理解和抽象,研發了一整套任務自動排程和監控的資料工廠體系,藉助IPC跨程式通訊機制和SHA共享記憶體技術,完成流水線之間資料傳遞和通訊的需求,整個工廠的生命週期內,需要負責將上層輸入的巨量圖片資料自動進行高效加工,生產出資料特徵傳回服務端供商家使用。
整個工廠框架涉及到的模組主要有:
任務解析器:負責將演算法描述的任務流圖進行解析,完成資料流動管道的梳理以及任務與管道的繫結。
任務排程管理器:主要負責流水線上任務的排程管理工作(例如:將更多資源排程給管道比較擁擠的任務、沒有資料的時候進行休眠)。
容災恢復:當流水線在加工資料的過程中遇到異常退出的時候,建立新的程式繼續工作,保證流水線資料的正常流轉。
任務監控器:負責監控流水線任務的執行情況,如有死迴圈或者異常,能夠及時發現。
跨程式資料管道:負責將資料從流水線傳遞到另一條流水線。
通過以上主要模組,我們可以順利搭建好工廠的執行環境,並能夠處理基本的異常恢復和監控,進而保證工廠穩定良好的執行環境。
四、流式程式設計
演算法需要對AI任務進行分割(比如前處理、inference、後處理),包裝成獨立的計算模組,然後使用自動流水線框架API描述任務的輸入輸出介面的資料結構、任務間的依賴關係,跨程式通道的資料結構,參考工廠的組建過程程式碼如下圖所示:
描述工廠構造過程的主要模組如下圖所示,開發描述過程主要包含了AI任務的輸入、輸出資料結構的定義,執行函式名(指向指向函式體)。資料結構支援樹形資料結構定義。工廠裡面的解析器負責將各個任務關係進行解析,根據輸入輸出資料結構定義,通過跨程式記憶體管理器構建資料流管道。通過任務間依賴關係構建任務資料流向關係,為了提升記憶體利用效率,我們專門為圖片設計了多流水線共享管道,並構建圖片索引佇列,任務只需要根據圖片索引從共享管道獲取需要處理的圖片,省掉了圖片資料的流動,大大提升圖片的儲存耗時同時也節省記憶體。利用執行函式名構建執行任務實體,並塞入到執行程式的任務列表中。
五、任務排程系統
CPU 是多核時是支援多個執行緒同時執行。但在 Python 環境中,無論CPU是多少核,Python的直譯器同時只能由一個執行緒獲取。其根源是 GIL 的存在。GIL就是一把巨大的鎖,執行緒執行的時候需要獲取這把鎖才能獲取直譯器,同一個程式只有一把GIL鎖,拿不到這個鎖的執行緒只能等待其他執行緒把GIL鎖釋放後再執行,因此Python中的多執行緒併發都是邏輯上併發物理上序列執行,有興趣的同學可以自行了解GIL的前程往事。
由於GIL鎖的存在用多執行緒實現AI Inference子任務併發提高效率的願望落空之後,只能選擇多程式利用CPU的多核實現高併發提高資料吞吐量,在多程式開發中,每條流水線中的任務需要滿足百分之九十以上時間只會使用同一類資源(例如只需要CPU或者GPU),如果達不到這個條件就沒辦法最大限度利用硬體資源。
1、任務流水式執行過程
假如有十個任務,四條流水線為例進行說明,任務以及資料傳遞通道如下圖所示,各個任務的耗時以及所需要的CPU/GPU資源不一樣,在部署任務的過程中會考慮到資源和耗時都需要保持相對平衡,然後將各個任務部署在不同的流水線上,任務部署後資料的流向看起來會毫無規則,實際上入流水一般沒有阻礙。各個任務從理論邏輯上面看是序列執行,但是從各個任務的執行實際和排程機理上看各個任務都是獨立併發執行。
每條流水線任務的執行受到獨立任務排程管理器控制,在迴圈資料加工過程中,會存在某些輸入管道或者輸出管道比較擁堵、某些任務需要批量跑資料但是任務的管道容量或者資料不足以容納這麼多Batch的資料,當然還有其他一些條件就不一一列舉,這時候會優先排程其他任務,任務經過資料加工廠的高度抽象化,可以很方便的進行資料任務的排程優化,進一步提升資料加工效率。
2、流水線通訊機制
從上面的流水線執行邏輯圖上可以看出,各個流水線的輸入資料和輸出資料具有不確定性,這就導致了一個問題:為了節約硬體資源,當流水線的所有輸入管道都沒有資料的時候流水線會進入自動休眠狀態,但是誰來負責將流水線喚醒?
毫無疑問,首先想到的是用訊號量,以流水線1為例說明,流水線1有三條輸入管道,分別為上層輸入的後設資料、流水線2上面Task7的輸出管道、流水線3上Task9的輸出管道,也就是說流水線需要等待三個獨立的訊號量,訊號量有個問題就是隻要一個訊號量獲取不到當前流水線就會被休眠,這勢必會堵住當前流水線工作,流水線需要的工作機制是隻要有一個管道有資料就會工作,只有所有管道都沒資料的時候才會休眠,這只是考慮到三個管道的情況,還沒有考慮輸出管道都滿了的時候,如果都考慮的話甚至會存在多達幾十個訊號量,這極大增加了流水線工作的複雜度和效率,是萬萬不能接受的。
我們捨棄了用訊號量的方式控制流水線的工作狀態,以資料共享管道為介質,讓管道主動喚醒休眠狀態的流水線。在工程初始化的時候,會分析出資料和任務的關係圖、資料流轉圖,通過這兩個圖將相關聯的流水線繫結到各自管道上,只要管道有資料輸入就會喚醒下游的流水線,通過這種方式就很好的解決了訊號量爆炸的問題。
六、高效能跨程式共享管道
在多程式實現的流水線方案中,由於每個程式的資料都是相互獨立的,一個程式產生或修改的資料對其他程式而言它是無感知。如何提高程式間的資料傳遞是能否高效實現併發的關鍵點。
在Python中實現多程式共享記憶體的現有解決方案主要有:管道、multiprocessing.Queue,multiprocessing.Value,multiprocessing.Array,前兩個主要是利用Pipe實現的管道在核心分配一個緩衝區並進行管理,後兩個主要是利用mmap來儲存ctypes型別的資料結構,後兩個的資料儲存效能較前兩個高很多,缺點也比較明顯,Value/Array只支援對Ctypes基本資料型別的儲存,對複雜的資料物件型別不支援,我們的自動流水線方案中需要儲存自定義的類結構,顯然達不到效能高靈活性高的兩高要求。既然現有的方案不能滿足我們的需求,需要我們自己開發支援複雜物件的高效能跨程式通道。
1、管道主要模組
從自定義物件到儲存底層,主要設計瞭如下圖所示的幾個功能模組,跨程式共享記憶體使用了SHA技術作為儲存底層邏輯,SHA相比mmap各有優勢,兩者的區別和優劣網上有很多文章進行詳細的講解,我們選擇SHA的一個主要原因是它比較容易就能夠支援動態擴充套件儲存節點,比較方便的支援了List資料結構。
從一個自定義物件資料到儲存在SHA共享記憶體,中間主要經歷了物件結構的分拆、資料序列化、資料維度和型別的處理、記憶體塊拷貝,讀操作跟這個過程的逆操作,讀和寫的主要區別在於寫資料會存在一個資料拷貝的過程,讀資料將這個過程優化掉了,直接將共享記憶體的資料格式轉換為目標資料,讀資料的效能得到了大大的提高。
資料分拆過程主要是對類資料結構進行遞迴解析,將資料結構的屬性分拆為最基本的資料型別,為每個屬性分配了一個屬性佇列方便資料操作。利用反射機制高效進行資料序列化和反序列化。
2、資料序列化和反序列化
自定義資料結構的序列化和反序列化操作如下圖所示,主要有兩個過程:
類物件 ↔ 數字序列,可以將類結構組織為樹狀結構,非葉子節點表示為自定義資料結構,葉子節點表示由基本資料型別(float/int/…)組成的數字序列。
數字序列 ↔ Ctypes資料,主要將numpy的資料結構轉換為ctypes的型別,並將資料維度進行歸一化/反歸一化。
使用python的反射特性,通過傳入的資料結構型別資訊解析出樹形節點,並轉換為ctypes的資料型別,以這樣的陣列作為資料管道的儲存元素,構建通訊管道。
七、流水線容災恢復
作為一個長期執行的工廠體系,能夠持續穩定高效率地將後設資料加工為產品是我們的追求目標之一,提高系統質量加強程式碼魯棒性減少bug或者異常是基礎工作,但是一旦遇到異常能夠恢復也是必不可少的部分,我們還是有必要引入基本的異常恢復功能模組。
因為Python語言GIL鎖的原因我們的多流水線對應的是多程式,只有在多程式下才能享受高併發特性。多程式帶來高併發的同時也帶來了高複雜度,流水線之間的工作相對獨立,一個程式發生異常其他程式是不會知道的,其他程式依然會執行,但是中間流水線中斷工作後,導致整條資料管道中斷,工廠將無法工作,為此,我們專門做了多流水線監控機制,如下圖所示:子執行緒會每隔一段時間檢測流水線狀態,如果發現有某條流水線發生異常,則啟動異常恢復機制,更換流水線程式讓這條流水線死灰復燃。
流水線能夠恢復的一個前提是程式發生異常的時候需要將現場資料進行儲存,待流水線啟動異常恢復更換程式,下一個程式再讀取上一次現場資料繼續往下加工,這個控制流程如下圖所示:
八、效能資料
1、跨程式共享管道效能測試
在CPU(Intel Core I5-45903.3GHz*4)的機器上,測試讀取(1080*1920*3)的圖片一千次,分別測試了基於Pipe管道的Queue佇列、基於mmap儲存Array、基於SHA自定義的管道三種儲存方案,測試效能如下表:
跨程式共享記憶體佇列 | 寫入耗時(ms/次) | 讀取耗時(ms/次) |
Multiprocessing.Queue | 0.01 | 6.55 |
Multiprocessing.Array | 0.91 | 0.014 |
自定義支援序列化跨程式管道 | 0.97 | 0.016 |
通過測試發現,我們實行的基於SHA支援複雜資料結構的管道讀寫資料效能都很接近Python自帶的Array方案,但是Array方案的缺點就是不支援序列化複雜物件,只支援基本資料型別。相比同樣支援序列化的Queue佇列,我們自己實現的管道在讀取耗時上比Queue快了400倍,在資料寫入上因為Queue採用的是非同步寫入,所以測試出的資料不能真實反映真正儲存到共享記憶體鎖需要的時間,相信真正寫入的耗時遠遠大於測試出的資料。
2、自動流水線整體框架效能
我們分別對序列版本、線上的人工流水線並行版本、當前的流水線任務自動排程版本效能進行了極限測試對比,測試環境如下:
測試條件 | CPU | GPU | 記憶體 | 視訊記憶體 | 攝像頭解析度 | 攝像頭幀率 | AI Inference框架 |
I5 6核*2.8GHz | GTX1070 | 8G | 8G | 1080*1920 | 5 | Tensorflow |
測試結果對比如下:
版本 | 支援最大攝像頭路數 | 單個攝像頭最大人數 | 單幀AI Inference耗時(ms) | CPU 利用率 | GPU 利用率 | GPU 視訊記憶體(G) |
序列版本 | 8 | 12 | 26 | 155% | 36%-40% | 4.3 |
流水線版本 | 16 | 12 | 13.7 | 259% | 60%-70% | 5.4 |
自動任務流水線管理 | 18 | 12 | 11.9 | 282% | 70%-80% | 5.2 |
從實際測量資料來看,人工流水線版本帶來的效能提升是完全符合預期的:支援視訊的路數多了一倍,但是手動構建流水線複雜度高,影響演算法的迭代效率。
受益於自動流水線框架的高度抽象,很多工排程優化能夠很方便的應用到流水線上面,從資料測試上也能看出這個優勢,自動流水線框架效能在人工流水線版本之上得到進一步提升,從16路攝像頭提升到了18路攝像頭,單幀AI耗時從13.7毫秒降低到11.9毫秒,效能提升13%,硬體資源的使用率也是符合設計預期的。
自動流水線版本對比人工流水線版本資料提升如下:
CPU的使用率提升了20%
GPU的使用率提升了10%
攝像頭數提升12.5%
單幀AI耗時降低13%
通過自動流水線框架對AI任務的執行流程進行描述,開發複雜度得到了很大程度的降低,程式設計模式得到了演算法同學的認可,模型的執行效率得到大大提升,模型迭代效率得到了質的提升。
九、專案業務效果
目前我們客流數字化專案已經上線執行有較長一段時間,已經有很多商家接入了我們的客流系統,所採集到的特徵資料已經在相應的產品中得到應用,一些商家基於我們分析出的資料對商超佈局、商品擺放順序以及商品種類進行了相應調整,購物體驗和交易量都得到了相應提升。
我們分別以區域熱力圖和區域動線圖來說明我們的業務展示效果,區域熱力圖和區域動線圖分別展示如下:
區域熱力圖的計算方式為:將門店的所有攝像頭資料進行整合分析,最終將反應人流密度的資料對映到門店的平面CAD圖上形成區域熱力,這主要向商家展示在一天內門店各個區域各時段的人流密度,讓商家能夠清楚直觀看到各個區域的人流熱度。區域動線圖展示了各個區域的人流以及人流的流向,基於這兩個資料圖,商家能夠清晰瞭解到門店各個區域的人流密度情況以及消費者在門店的流轉狀態,通過人流動線圖可以分析出商品的擺放是否合理,消費者是否比較方便選到自己中意的商品,有了這些資料,商家在調整銷售策略的時候便有了資料依據和方向。
總結和展望
本文主要介紹了自動流水線任務排程系統在客流專案中的應用,相信流水線的程式設計思想在工程實踐中優化效能上還有很大的利用價值,相信很多工業界成熟優秀的執行模式都可以運用到軟體工程中,對我們以後在執行效率提升方面的探索研究具有很強的指導意義。
自動任務排程系統還可以應用到更多其他專案中,在訓練演算法模型階段也可以嘗試接入,提高訓練效率降低訓練時間。自動流水線框架的目標使用者是演算法同學,提高演算法模型的迭代效率是我們的第一準則,在保證效能的前提下儘可能的方便簡單。
儘管從上面的測試資料看,硬體資源以及得到了極大程度的利用,但是依然相信還有很多可以挖掘提升的地方,模型執行效能和穩定性都能夠進一步提升。更多的商家和業務在接入中,我們在不斷優化技術的同時,也在和行業商家一起不斷打磨產品體驗和細節,爭取讓客流專案更好地賦能商家賦能新零售。
阿里技術重磅釋出!年度精選集開放下載
《數字經濟下的演算法力量:阿里演算法年度精選集》電子書,精選數十篇年度演算法頂級乾貨,合計300+頁,為你呈現阿里最新的演算法實踐案例。
長按識別以下二維碼,關注“阿里巴巴機器智慧”官方公眾號,在對話方塊內回覆“演算法”,即可免費線上閱讀或下載此書。
↑ 這裡除了演算法乾貨
一無所有
你可能還喜歡
點選下方圖片即可閱讀
史上首張中國面孔!阿里90後李響成為 CNCF 全球 9 位 TOC 之一
終於等到你!阿里正式向 Apache Flink 貢獻 Blink 原始碼
關注「阿里技術」
把握前沿技術脈搏
相關文章
- 如何提升程式設計師的“效能”程式設計師
- fre 更新了,框架設計重思考……框架
- Java程式設計中“為了效能”儘量要做到的一些地方Java程式設計
- 驚了!修仙=程式設計??程式設計
- 換掉Typora!這款為程式設計師量身打造的筆記應用,太香了!程式設計師筆記
- 為什麼我喜歡程式設計 程式設計充滿了樂趣程式設計
- 推薦 12 個提升程式設計師軟技能與效率的必備工具,愛了愛了 ?程式設計師
- 自從有了阿里雲就不需要雲端計算運維工程師了?阿里運維工程師
- 反轉!BAT程式設計吸金榜來了,AI程式設計師刷爆了......BATAI程式設計師
- 學習了風變程式設計後,Python為我敞開了大門程式設計Python
- “閃總”曹力:創業是為了自由,程式設計是為了快樂(圖靈訪談)創業程式設計圖靈
- 扎心了,程式設計師!程式設計師
- 幹了3年程式設計師,我開竅了程式設計師
- 為什麼你該開始學習程式設計了?程式設計
- 知道了這些 MongoDB設計技巧,提升效率50%MongoDB
- 挺後悔,我敷衍地回答了“程式設計師如何提升抽象思維“程式設計師抽象
- C++程式設計師看過來,你會為了效能而犧牲程式碼簡潔性嗎?C++程式設計師
- 面試:為了進阿里,重新翻閱了Volatile與Synchronized面試阿里synchronized
- 過了 35 歲, 程式設計師真的沒前途了嗎?程式設計師
- 年終了,程式設計師這樣談加薪就穩了!程式設計師
- 程式設計師刪庫跑路了?程式設計師
- 老程式設計師都去哪了?程式設計師
- 為什麼結束了十年的程式設計生涯?程式設計
- 那些學了 Python 的程式設計師,程式設計能力都“退化”成什麼樣了?Python程式設計師
- 當程式設計師寫不出程式碼了……程式設計師
- 終於有了讓程式設計師脫離程式碼的工具了程式設計師
- 程式設計師,請你不要在坑程式設計師了?程式設計師
- 程式設計師的春天來了?能抓住嗎?高階工程師速成指南!程式設計師工程師
- 為了收集和整理程式設計的常用單詞,我寫了個背單詞應用程式設計
- 阿里程式設計師相親被嫌棄,只因穿了雙特步鞋!阿里程式設計師
- JWT不是為了授權而設計的JWT
- 萬字詳解 | Java 流式程式設計Java程式設計
- 為了設計這個計程車遊戲,開發者真的成為了一名網約車司機遊戲
- 程式設計師薅羊毛神器來了!程式設計師
- 做程式設計師快30天了程式設計師
- 老程式設計師都去哪兒了?程式設計師
- 我就差一個程式設計師了!程式設計師
- 你可以去當程式設計師了程式設計師