有在做套件開發的開發者們應該不陌生 @inbounds 這個 macro,在很多現代程式語言中也有。

在存取陣列時,為了安全性與正確性的考量,避免存取到陣列範圍以外的記憶體位置,很多語言都設置了邊界檢查(bounds check)。

邊界檢查會檢查所存取的索引值是否在陣列的範圍內,但是這樣的檢查會有些微的效能損耗,尤其在迴圈內的情況更有可能被累積而放大,關於 Julia 的 邊界檢查可以參考官方文件 Bounds checking

如果可以確定所存取的索引值一定在範圍內,我們就可以把邊界檢查給移除,以加速陣列的存取。如以下範例:

1
2
3
4
A = rand(3, 4)
@inbounds for i = 1:size(A, 1)
println(A[i, :])
end

或是

1
2
3
4
A = rand(3, 4)
for i = 1:size(A, 1)
@inbounds println(A[i, :])
end

@inbounds 會將程式碼區塊中的邊界檢查給移除,可以參考 @inbounds官方文件。使用時必須注意存取的索引值,否則小則存取的值錯誤,大則可能導致程式崩潰。

先養成好的索引習慣,再考慮將效能提升,加入 @inbounds。相關的資訊也紀錄在官方的效能建議中。

留言與分享

剛好看到一些跟編譯器相關的議題,所以來紀錄一下。

在一些語言中會有行內函式(inline function)的設計,使用的話一般會讓程式的效能變好。

最知名應該是 C 跟 C++ 的 inline

Inline function 會在編譯時期直接將函式內容展開到程式碼中,不過展開與否是由編譯器決定的,inline 的標記只是告訴編譯器這個函式可以成為 inline function。

Inline expansion 就是編譯時期會由編譯器執行的一個動作,看起來與 macro expansion 相似,但不同的是 macro expansion 是在前處理(preprocessing)時期做的,會直接展開在原始碼裡頭,而 inline expansion 則是在編譯時期做的,會在呼叫位點(call site)直接展開。

展開後編譯器便可以進行最佳化,執行時,就不需要做函式呼叫,也不會在 function stack 上多配置空間。一般使用在短小的函式上會有好處,在巨大的函式上使用不一定會有好處。然而過多的 inline function 反而可能造成過多的指令快取的消耗,造成反效果。

在 Julia 中,編譯器會自動偵測哪些函式可以被展開,會自動做 inline expansion。一般短小的函式會自動被編譯器判定要 inline,不過也可以由程式設計師自己指定哪些巨大函式可以 inline,可以參考[文件]](https://docs.julialang.org/en/v1.2/base/base/#Base.@inline)。

除了 @inline 以外,還有 @noinline。為了避免過多的 inline 反而傷害效能,也可以標記一些短小的函式不要 inline。

範例:

1
2
3
@inline function bigfunc(x)
...
end

Julia 什麼時候 inline expansion?

我們來實驗看看,以下有兩個函式,foobar。其中讓 foo 為 inline。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
julia> @inline foo(x) = 3x
foo (generic function with 1 method)

julia> bar(x) = foo(x)^2
bar (generic function with 1 method)

julia> bar(5)
225

julia> @code_lowered bar(5)
CodeInfo(
1 ─ %1 = Main.foo(x)
│ %2 = Core.apply_type(Base.Val, 2)
│ %3 = (%2)()
│ %4 = Base.literal_pow(Main.:^, %1, %3)
└── return %4
)

julia> @code_typed bar(5)
CodeInfo(
1 ─ %1 = Base.mul_int(3, x)::Int64
│ %2 = Base.mul_int(%1, %1)::Int64
└── return %2
) => Int64

你會發現在 lower 的階段仍保留有 foo 的函式呼叫的過程,但是到了 typed 的階段就已經剩下計算的部份了。

所以其實在進入 LLVMIR 以前就已經先做完 inline expansion 了。

相關技術:Inline caching

留言與分享

來分享一下最近貢獻開源專案的小心得,雖然我自己貢獻開源專案的次數跟時間不是很多,不過一個 PR 可以產生不少文字跟討論算是值得紀錄一下的。

我自己在 Julia 的一個統計相關的專案上發了一個 PR,希望補齊在標準化上的一些功能,並且可以支援一維的陣列標準化。

繼續閱讀

之前介紹過將值放在參數型別上。

今天來介紹一下如何可以做到類似運算的效果。

1
2
3
4
5
struct A{T}
end

a1 = A{5}()
a2 = A{3}()

在一些應用場景上會希望將參數欄位上的值做運算,例如加總。

這時候我們可以這樣做:

1
2
3
4
5
6
import Base:+

function +(::A{T}, ::A{S}) where {T, S}
x = T + S
return A{x}()
end

如此一來,就可以簡單搞定囉!

1
println(a1 + a2)
1
A{8}()

留言與分享

我在使用的時候有注意到在參數化型別中使用值的方式與傳統封裝的方式有效能上的差異。

所以我就做了一些測試。

在參數化型別中使用值:

1
2
3
struct A{T}

end

傳統型別封裝:

1
2
3
struct B
x::Int64
end

全文出現的程式碼為實際測試程式碼

因為 Julia 有提供好用的 @code_llvm@code_native 來觀察一行程式碼實際被轉換成 LLVM 或是組合語言的時候會產生多少行的程式碼,藉此我們可以用低階程式碼來評估是否有效率。程式碼的行數愈少是越有效率的。

建立

我們來測試一個物件被建立需要多少行的程式碼。

A - LLVM

1
@code_llvm A{5}()
1
2
3
4
5
6
7
;  @ REPL[1]:3 within `Type'
define nonnull %jl_value_t addrspace(10)* @japi1_Type_12238(%jl_value_t addrspace(10)*, %jl_value_t addrspace(10)**, i32) #0 {
top:
%3 = alloca %jl_value_t addrspace(10)**, align 8
store volatile %jl_value_t addrspace(10)** %1, %jl_value_t addrspace(10)*** %3, align 8
ret %jl_value_t addrspace(10)* addrspacecast (%jl_value_t* inttoptr (i64 140407726014496 to %jl_value_t*) to %jl_value_t addrspace(10)*)
}

B - LLVM

1
@code_llvm B(5)
1
2
3
4
5
6
;  @ REPL[1]:2 within `Type'
define { i64 } @julia_Type_12221(%jl_value_t addrspace(10)*, i64) {
top:
%.fca.0.insert = insertvalue { i64 } undef, i64 %1, 0
ret { i64 } %.fca.0.insert
}

A - Assembly

1
@code_native A{5}()
1
2
3
4
5
; ┌ @ REPL[1]:3 within `Type'
movq %rsi, -8(%rsp)
movabsq $140407726014496, %rax # imm = 0x7FB338A20020
retq
; └

B - Assembly

1
@code_native B(5)
1
2
3
4
5
; ┌ @ REPL[1]:2 within `Type'
movq %rsi, %rax
retq
nopw %cs:(%rax,%rax)
; └

取值

接著測試從物件當中取值出來的效能。

定義取值的方法:

1
2
get_value(::A{T}) where {T} = T
get_value(b::B) = b.x

事先建立好物件:

1
2
a = A{5}()
b = B(5)

A - LLVM

1
@code_llvm get_value(a)
1
2
3
4
5
;  @ REPL[8]:1 within `get_value'
define i64 @julia_get_value_12274() {
top:
ret i64 5
}

B - LLVM

1
@code_llvm get_value(b)
1
2
3
4
5
6
7
8
9
;  @ REPL[5]:1 within `get_value'
define i64 @julia_get_value_12630({ i64 } addrspace(11)* nocapture nonnull readonly dereferenceable(8)) {
top:
; ┌ @ sysimg.jl:18 within `getproperty'
%1 = getelementptr inbounds { i64 }, { i64 } addrspace(11)* %0, i64 0, i32 0
; └
%2 = load i64, i64 addrspace(11)* %1, align 8
ret i64 %2
}

A - Assembly

1
@code_native get_value(a)
1
2
3
4
5
; ┌ @ REPL[8]:1 within `get_value'
movl $5, %eax
retq
nopw %cs:(%rax,%rax)
; └

B - Assembly

1
@code_native get_value(b)
1
2
3
4
5
; ┌ @ REPL[5]:1 within `get_value'
movq (%rdi), %rax
retq
nopw %cs:(%rax,%rax)
; └

給大家參考。

留言與分享

應該不少人看到這個標題會摸不著頭緒到底要做什麼,但是看完 Julia 中常見的程式碼你就會了解了。

1
Array{Any, 2}

有沒有曾經納悶過那個數字 2 到底是怎麼進到參數的位置上的呢?

參數的位置不是只能放型別(type)嗎?

這同時也是我困惑已久的問題,就搜尋了一下,果不其然被我找到了方法:

1
2
struct A{T}
end
1
A{5}()

原來這麼簡單就可以完成了!語法上並沒有限定一定要是型別,要放型別以外的東西似乎是可以的。

我目前測試了可以的有:Int64、Float64、Complex、Char、Bool、Symbol,所以估計數字應該都是可以的。

不行的有:String、Array,估計物件或是陣列都是不行的。

定義範圍

不過使用上並沒有任何限制會有點危險,所以還是定義一下範圍會比較好,像是:

1
2
3
4
5
6
struct A{I}
function A{I}() where {I}
isa(I,Integer) || error("bad parameter")
new()
end
end

這樣就可以限制參數要是整數的範圍。

從參數取值

那我們能不能從型別的參數當中取值呢?

可以。

1
get_value(::A{I}) where A{I} = I

如此一來,我們就可以從型別中拿到值了。

好處?

這麼做有什麼好處?

當你把值的資訊放到型別當中,型別就多了一些資訊可以提供編譯器處理,這對於要自己設計型別階層可是非常好用的。

例如像是你可以將陣列的長度資訊儲存到型別上,這樣編譯器就可以處理陣列的長度資訊了。

這樣的程式風格會跟 dependent type language 有些相似了。

大家可以玩玩看。

留言與分享

Julia 的物件導向

今天特別要來談談Julia這個語言的物件導向模型。

我已經聽到有些人跟我說「Julia才沒有物件導向」或是「我不承認這是物件導向」等等這類的話。

不過我得要跟大家說,物件導向不是只有大家看到的C++或是Java那個樣子的物件導向模型。

介紹 Julia 的物件導向模型

如果你的背景是C++, Java, Python, Ruby等等語言的話,那你應該非常熟悉物件導向的幾個要素:

  1. 封裝
  2. 繼承
  3. 多型

談到封裝,Julia幾乎沒有封裝的概念。

是的,Julia不封裝!

Julia宣告的是型別,不是類別;型別他只宣告了有哪些欄位,他也不會把方法宣告在型別裡頭。

所以Julia的型別感覺上會比較像是C的struct,只存資料或狀態,不帶任何的方法。

那方法怎麼辦?

方法會由 Julia 的多重分派(multiple dispatch)引入。

也就是說,我只要去設計任何函式,這個函式他所用到的參數型別,那這個函式就是這些參數的方法。

Julia會視函式呼叫的參數型別組合,來決定到底要執行哪一種方法。

看完之後應該會很傻眼,既然沒有封裝,然後又不帶方法,那這樣要怎麼實現物件導向呢?

Julia 的型別系統

概念上來說,Julia會藉由兩種型別來建立型別階層(type hierarchy):抽象型別跟複合型別

1
2
3
4
5
6
7
abstract type Number end
abstract type Complex <: Number end
abstract type Real <: Number end

struct Integer <: Real
x
end

以上宣告了 3 個抽象型別以及一個複合型別。

抽象型別只有一行宣告,也就是說明這代表什麼樣的概念,他不具有欄位。

<: 代表的是 is subtype ofabstract type Complex <: Number end代表的是ComplexNumber的子型別。

複合型別就像Integer,他有一個field x,他是Real的子型別。

子型別可以是抽象型別或是複合型別,但相反的是,父型別只能是抽象型別。

以上我們描繪了一個型別階層,位於最頂端的是Number,所有人都是Number的子型別。

而複合型別只能位於這棵型別階層的末端的位置。

在Julia中,所有的型別都是Any的子型別,相反,所有型別的子型別則是Union{}

這樣的階層完全不俱備傳統物件導向的繼承概念,雖然觀念上也滿足is-a的關係,但是他並沒有從父型別上繼承任何東西,也沒有東西可以被繼承。

你可能會問:那這樣的型別階層有什麼用呢?

這就要配合多重分派來用拉~~

Julia 的多重分派

大家比較熟悉的應該是single dispatch,也就是區別foo.method()bar.method()的不同。

傳統的物件導向當他呼叫foo.method()bar.method()這兩者時,同樣都是呼叫method()這個函式,可是到底要呼叫誰的實作呢?

Single dispatch會告訴你:去找擁有這個函式的人,也就是.前面的那個傢伙。

但是Julia的設計理念是:函式並不屬於任何人,也就是不會有人擁有函式。

所以Julia裡的函式會是method(foo)method(bar)這樣的形式。

Julia設計上很聰明,他會告訴函式去執行擁有跟參數型別相同排列組合的那個實作。

這也就是多重分派了,他不依賴單一的型別去判斷,而是所有的參數型別的組合!

所以當你宣告了一個像這樣的函式:

1
2
3
function foo(a, b, c)
....
end

他會接受任意的3個參數,當沒宣告參數型別時就自動是Any

那以下這個狀況:

1
2
3
function foo(a::Int64, b)
....
end

就只接受第一個參數型別是Int64,而輸入兩個參數的情況了。

OOP in Julia

以這樣的type system以及多重分派作為建構物件導向的核心機制。

我們不能在以傳統的思維去想物件導向。

我們需要思考的是物件導向的核心是什麼?

物件導向的引入主要幫助我們去塑造一個物件概念,並且幫助我們減少重複的程式碼,提高程式碼的再利用(reuse)。

我們訴求的是高內聚,低耦合的程式碼。

在Julia中,複合型別提供了一個類似物件、結構的角色,由於沒有繼承關係,也不會有過高的內聚。

繼承所需要的元素可以由合成(composition)來達成,但相對犧牲了部份的欄位再利用的可能性。

方法的部份還原成了最初始的函式的樣貌,而函式本身是更加自由的。

藉由多重分派可以更加細緻地定義函式的行為,可以在不同的參數型別上提供多樣的語意。

例如,*(Int64, Int64)的含意是數字的乘法,*(String, String)則是字串的串接,同樣的運算子或是函數名稱,可以提供不同context下,不同的語意,這不是很像人類的語言習慣嗎?

在Julia的精神中,我們關注的是context跟行為,去定義不同context下的函式以及語意(行為)是重要的。

這方面Julia提供了絕佳的多型(polymorphism),但完全不支援繼承與封裝,對資訊的存取權更是無法控制。

但他更強調的是readability!如果設計者考慮到context,那讀程式碼的人就更能進入這個context去思考。

在函式的使用上更是毋庸置疑的靈活而鬆耦合,所以在程式碼的撰寫上,可以自然的達成高內聚,低耦合,更可以提升readability,最後的再利用就端看設計者最佳化的功力拉!

Julia是一個達成物件導向精神的語言!

State-centered and behavior-centered OOP

The state-centered OOP

傳統大家認知的物件導向,大多都由一個概念或是實體所構成。

大家在做程式設計的時候,大家所關注的大多是一個變數的值是如何變化或是一個物件的狀態如何變化

所以很自然的會從一個實體或是概念著手,進而去設計他應該有怎樣的properties或是做什麼事情。

會慢慢凝聚出一個概念,並且把這個概念化成一個個的class,並把這個class描繪清楚。

class描繪仔細了以後,就去關注class跟class之間的互動關係,進而完成整個演算法或是架構。

The behavior-centered OOP

在Julia的物件導向則是敘述關係或是行為

Julia會以函式的角度出發,Julia中的物件導向指的是design a set of methods這回事。

Julia會使你關注在參數之間的關係,他們應該如何一起完成工作?參數如何被dispatch?函式跟函式之間會不會衝突?

型別應該會有什麼樣的行為?*用在數字上跟用在字串上的語意有沒有一樣?

方法其實是在圈定一群型別,他們會一起作用,就是這麼簡單。

所以Julia的物件導向觀點都會圍繞在設計方法上面,而這樣的觀點又可以讓Julia很自然的融入functional programming當中。

這讓Julia可以花比較少力氣在斤斤計較欄位的設計上,也花比較少力氣在思考繼承關係上。

相對花比較多力氣在這個方法被宣告、進入整個語言系統當中的運作情形,也就是對整個系統的影響上。

留言與分享

很多初學java的人都會有這樣的疑問:

我該用類別, 抽象類別還是介面?

尤其在學 java 之前沒有碰過物件導向概念的人。
在多種選擇的情況下,我該用哪一個比較好呢?

我們可以分幾個觀點來看這個問題

概念解釋觀點

在物件導向的語言裏面,物件跟類別是被抽象化的概念。
也得力於這樣的抽象化,我們可以把某些東西廣義化應用到不同的層面。
這樣的抽象化有賴於封裝、繼承、多型的落實。
封裝可以將概念相同的程式碼放在一起,這些程式碼做同一件事,所以他們代表同一個概念。
同時也比較好維護。
繼承可以提高 reusabiity 的機會,同時也展現了概念上的相似性
多型就是一種同中求異的概念。
位於同一個繼承體系之下,可以有不同的行為。

承續我先前寫過的文章
繼承是is-a的概念,而實作介面是has-a的概念。
這些概念是不相違背的,所以我們可以簡單的把類別、抽象類別、介面分開。
類別跟抽象類別應該位於繼承體系之下,因為抽象類別也有is-a關係。
但是相對於(具體)類別,抽象類別是不允許被實體化(instantiate)的。

為什麼?
舉個例,Cat 跟 Dog 都繼承於 Animal,因為 Cat is an Animal and Dog is an Animal.
這時我們會把 Animal 寫成抽象類別,因為 Animal 被實體化之後不具任何意義。
他只是我們在繼承體系上或是概念上的類別,他並不是一個具體的物件,不具屬性跟行為。
所以 Animal 不應該被寫成(具體)類別。

那不能被實體化的類別我還需要去定義他的屬性跟方法嗎?
當然要!
回到我們的例子,通常只要是 Animal 就會 eat()move(),或是我想給 Animal id 或是 name
這時我們不會把這些東西寫在 Cat 或是 Dog 裏面。
我們會把他通通寫到 Animal 裏面,如此一來下面的子類別就可以簡單的具有這些屬性跟方法了。

這時我們來講講介面,就如同我前面提到的。
介面的實作是has-a的關係。
所以他提供了在一個繼承體系之外的功能性
他可以讓繼承於 Animal 的 Bird 擁有 fly() 的行為。
他可以讓繼承於 Animal 的 Turtle 擁有 swim() 的行為。
這些都是無法從 Animal 繼承過來的東西。

那無法繼承過來的話,自己寫就好阿,大驚小怪!
錯!
這時我們就會看到
fly()(飛)的 Bird(鳥)。
transport()(運輸)的 Airplane(飛機)。
swing()(拍翅膀)的 Swan(天鵝)。
但是他們想表達的都是在天上飛的概念。
所以介面可以達到統一格式的效果。
大家都實作同樣的方法來表達同樣的行為。
或許行為的實際內容不一樣,但是在概念上是相同的。

軟體工程觀點

在前一個觀點當中提到抽象化。
抽象化使得同樣的一段程式碼可以提高他使用的頻率。
也達到**reusability (重複利用性)**的效果。

我們有了繼承體系來提高 reusability
在程式碼上,子類別不用重寫同樣的程式碼,只要從父類別繼承來就好。
相對於同樣的程式碼散落在各個子類別,當要維護的時候就要一個一個修改。
這樣父類別就控制了子類別的行為。
如果父類別的方法被修改的話,也會影響到子類別。

如果你不希望父類別的改變影響到子類別的話,這時請確認兩者是否真的要使用繼承關係。
如果是繼承關係但又不希望受影響,那就用多型吧!

我們使用介面來提高系統的flexibility (靈活性)
當你需要有相同概念的方法時只要實作同樣的介面就可以達到。
同時規定同樣的介面便於管理維護。
只要實作同樣的介面就可以簡單地擴充模組。
這是一個靈活的系統該有的特徵。

相對於多重繼承 (ex. C++)

相對於C++的多重繼承,java 只允許單一繼承。
在多重繼承當中,可以讓一個類別擁有很多身份
一個類別可以很多種東西。
這樣的概念其實可以讓人容易了解這個類別在做些什麼。
像是Computer is a Machine and Computer is a Calculator.
這樣我們可以理解電腦是一種會計算的機器。
但是相對應的他也帶來一些不必要的副作用。
繼承於多樣的父類別,也同時繼承到了父類別的屬性跟方法,但是並不是所有的屬性跟方法都是有用的,甚至有些屬性或方法根本用不到。
當你沒有要使用到的時候卻又繼承了太多的東西,這就造成了另一種的設計不良的狀況。
這會讓別人誤解子類別有擁有父類別的某些方法,但卻不知道那是不應該出現的。
造成誤用問題就更大了。
這樣的問題小至程式錯誤,大至系統崩潰,這就成了系統的弱點之一了。

留言與分享

So far, I’ve touched or been familiar with several programming language:

Some I familiar with are Java, Python, Julia

Some I’ve played are C/C++, Haskell, Matlab, R, Perl, Lisp, Prolog, javascript, php

And took a lesson of Programming Language and Software Engineering

Read lots of book about Software Engineering…

Recall the basic programming lesson you’ve took

The quiz or exam would always be:

What does **** stands for?

or

Please write down a program to ****

Just some what like that…

But!

If you pass these kind of quizes or exams, can you say that you can program in this programming language?

As my aspect, a good programmer must understand what compiler says.

That is, an excellent programmer must know what happened while the compile error is thrown out

And he must have the ability to deal with it.

An excellent programmer must know about what will compiler act.

An excellent programmer must know how to tune the perfromance.

An excellent programmer must familiar with the basic syntax without the help of IDEs.

An excellent programmer must have some basic concept about hardware, at least the machine you’re using.

So…

If you think that you become an excellent programmer after taking some lessons of one language
And you cannot do above of these.

Well…

It’s still a long way to go.

留言與分享

自從在雙主修時修了軟體工程之後,一直受益於這個領域,而且最近也會看一些軟體工程的書。

覺得軟體工程強調的是彈性

彈性

  • 可擴充性 (scalability):如何設計系統讓系統在擴充功能或是應用範圍變大的時候變得簡單
  • 可移植性 (portability):如何設計系統讓系統在不同的環境中也能夠順利運作
  • 低耦合性及高內聚性 (low-coupling and high-cohesion):如何設計系統讓系統內部的模組 (module)之間具有此項特質

可擴充性

就像是一個建築師可以好好的完成一間小屋子。

但是他有沒有辦法把小屋子延伸成摩天大樓?

或是已經完成的屋子能不能在添加其他的功能上去?

可移植性

同樣一張小屋子的設計圖,是否可以在不同的環境下建出同樣的屋子?

在沙漠中?在冰原中?在高山中?

如果可以,稱為可移植性。

低耦合性

小屋子當中的各個房間有不同的功能。

各房間通常是打通的呢?或是隔開的?

最好是不要把房間的功能都混在一起。

像是想在廚房看書然後就把廚房放個書櫃。

明明書櫃就該放在書房的,為什麼會出現在廚房。

或許可能會覺得這樣對使用者比較方便。

但是在軟體工程的角度來說,這是不好的示範。

高內聚性

小屋子當中相同功能性的東西最好都放在同一個房間比較好。

這個是相對於低耦合的良好示範。

那如何去決定哪些功能該跟哪些功能放在一起。

這就是工程師該去傷腦筋的了。

留言與分享

Yueh-Hua Tu

目標是計算生物學家!
Systems Biology, Computational Biology, Machine Learning
Julia Taiwan 發起人


研發替代役研究助理


Taiwan