檢視原始碼 程序

建立 Erlang 程序

相較於作業系統中的執行緒和程序,Erlang 程序是輕量級的。

一個新產生的 Erlang 程序會使用 327 個字的記憶體。大小可以用以下方式找到:

Erlang/OTP 27 [erts-14.2.3] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]

Eshell V14.2.3 (press Ctrl+G to abort, type help(). for help)
1> Fun = fun() -> receive after infinity -> ok end end.
#Fun<erl_eval.43.39164016>
2> {_,Bytes} = process_info(spawn(Fun), memory).
{memory,2616}
3> Bytes div erlang:system_info(wordsize).
327

這個大小包含了堆積區(包含堆疊)的 233 個字。垃圾回收器會根據需要增加堆積區。

一個程序的主要(外部)迴圈必須是尾遞迴的。否則,堆疊會不斷增長直到程序終止。

不要這樣做

loop() ->
  receive
     {sys, Msg} ->
         handle_sys_msg(Msg),
         loop();
     {From, Msg} ->
          Reply = handle_msg(Msg),
          From ! Reply,
          loop()
  end,
  io:format("Message is processed~n", []).

io:format/2 的呼叫永遠不會被執行,但是每次遞迴呼叫 loop/0 時,仍然會有一個返回位址被推入堆疊。正確的尾遞迴版本函式如下:

應該這樣做

loop() ->
   receive
      {sys, Msg} ->
         handle_sys_msg(Msg),
         loop();
      {From, Msg} ->
         Reply = handle_msg(Msg),
         From ! Reply,
         loop()
 end.

初始堆積大小

預設的初始堆積大小為 233 個字,這個設定相當保守,目的是支援具有數十萬甚至數百萬個程序的 Erlang 系統。垃圾回收器會根據需要增加和縮減堆積區。

在程序數量相對較少的系統中,透過使用 erl+h 選項,或是針對每個程序使用 spawn_opt/4min_heap_size 選項,增加最小堆積大小,可能可以提升效能。

這樣做的好處有兩個:

  • 雖然垃圾回收器會增加堆積區,但它是一步一步地增加,這樣比在程序產生時直接建立一個更大的堆積區更耗費資源。
  • 如果堆積區遠大於儲存在其中的資料量,垃圾回收器也會縮減堆積區;設定最小堆積大小可以防止這種情況發生。

警告

執行時期系統可能會使用更多記憶體,而且由於垃圾回收的頻率降低,龐大的二進制檔案可能會被保留更長的時間。

在沒有適當測量的情況下,不要嘗試進行這種最佳化。

在具有許多程序的系統中,可以將執行時間短的計算任務產生到具有較高最小堆積大小的新程序中。當程序完成時,它會將計算結果發送到另一個程序並終止。如果最小堆積大小計算得當,程序可能根本不需要進行任何垃圾回收。

傳送訊息

在 Erlang 程序之間傳送的訊息中的所有資料都會被複製,除了在同一個 Erlang 節點上的 refc 二進制檔案字面值

當訊息被傳送到另一個 Erlang 節點上的程序時,它會先被編碼為 Erlang 外部格式,然後透過 TCP/IP socket 傳送。接收的 Erlang 節點會解碼訊息並將其分發到正確的程序。

接收訊息

接收訊息的成本取決於 receive 表達式的複雜程度。一個匹配任何訊息的簡單表達式非常便宜,因為它會提取訊息佇列中的第一則訊息:

應該這樣做

receive
    Message -> handle_msg(Message)
end.

然而,這並不總是方便的:我們可能會收到目前不知道如何處理的訊息,因此通常只會匹配我們預期的訊息:

receive
    {Tag, Message} -> handle_msg(Message)
end.

雖然這樣很方便,但也意味著必須搜尋整個訊息佇列,直到找到匹配的訊息。對於具有長訊息佇列的程序來說,這非常昂貴,因此有一種針對常見情況的優化,即發送請求後不久就等待回應:

應該這樣做

MRef = monitor(process, Process),
Process ! {self(), MRef, Request},
receive
    {MRef, Reply} ->
        erlang:demonitor(MRef, [flush]),
        handle_reply(Reply);
    {'DOWN', MRef, _, _, Reason} ->
        handle_error(Reason)
end.

由於編譯器知道由 monitor/2 建立的參考不能在呼叫之前存在(因為它是一個全域唯一識別碼),並且 receive 只會匹配包含該參考的訊息,因此它會告訴模擬器只搜尋呼叫 monitor/2 之後到達的訊息。

以上是一個簡單的範例,它保證可以進行最佳化,但更複雜的程式碼呢?

選項 recv_opt_info

使用 recv_opt_info 選項讓編譯器印出關於接收最佳化的資訊。它可以被提供給編譯器或 erlc

erlc +recv_opt_info Mod.erl

或者透過環境變數傳遞:

export ERL_COMPILER_OPTIONS=recv_opt_info

請注意,recv_opt_info 並非要成為加入到你的 Makefile 的永久選項,因為它產生的所有訊息都無法消除。因此,在大多數情況下,透過環境傳遞選項是最實際的做法。

警告看起來如下:

efficiency_guide.erl:194: Warning: INFO: receive matches any message, this is always fast
efficiency_guide.erl:200: Warning: NOT OPTIMIZED: all clauses do not match a suitable reference
efficiency_guide.erl:206: Warning: OPTIMIZED: reference used to mark a message queue position
efficiency_guide.erl:208: Warning: OPTIMIZED: all clauses match reference created by monitor/2 at efficiency_guide.erl:206
efficiency_guide.erl:219: Warning: INFO: passing reference created by make_ref/0 at efficiency_guide.erl:218
efficiency_guide.erl:222: Warning: OPTIMIZED: all clauses match reference in function parameter 1

為了更清楚地說明警告指的是哪個程式碼,以下範例中的警告會作為註解插入到它們所指的子句之後,例如:

%% DO
simple_receive() ->
%% efficiency_guide.erl:194: Warning: INFO: not a selective receive, this is always fast
receive
    Message -> handle_msg(Message)
end.

%% DO NOT, unless Tag is known to be a suitable reference: see
%% cross_function_receive/0 further down.
selective_receive(Tag, Message) ->
%% efficiency_guide.erl:200: Warning: NOT OPTIMIZED: all clauses do not match a suitable reference
receive
    {Tag, Message} -> handle_msg(Message)
end.

%% DO
optimized_receive(Process, Request) ->
%% efficiency_guide.erl:206: Warning: OPTIMIZED: reference used to mark a message queue position
    MRef = monitor(process, Process),
    Process ! {self(), MRef, Request},
    %% efficiency_guide.erl:208: Warning: OPTIMIZED: matches reference created by monitor/2 at efficiency_guide.erl:206
    receive
        {MRef, Reply} ->
        erlang:demonitor(MRef, [flush]),
        handle_reply(Reply);
    {'DOWN', MRef, _, _, Reason} ->
    handle_error(Reason)
    end.

%% DO
cross_function_receive() ->
    %% efficiency_guide.erl:218: Warning: OPTIMIZED: reference used to mark a message queue position
    Ref = make_ref(),
    %% efficiency_guide.erl:219: Warning: INFO: passing reference created by make_ref/0 at efficiency_guide.erl:218
    cross_function_receive(Ref).

cross_function_receive(Ref) ->
    %% efficiency_guide.erl:222: Warning: OPTIMIZED: all clauses match reference in function parameter 1
    receive
        {Ref, Message} -> handle_msg(Message)
    end.

字面值池

常數 Erlang 項(以下稱為字面值)會保存在字面值池中;每個載入的模組都有自己的池。以下函式並非每次呼叫時都建立元組(只會在下次執行垃圾回收器時將其丟棄),而是將元組放置在模組的字面值池中:

應該這樣做

days_in_month(M) ->
    element(M, {31,28,31,30,31,30,31,31,30,31,30,31}).

如果將字面值或包含字面值的項插入到 Ets 表格中,則會被複製。原因在於包含字面值的模組未來可能會被卸載。

當字面值被傳送到另一個程序時,它不會被複製。當持有字面值的模組被卸載時,字面值將會被複製到所有持有該字面值參考的程序的堆積區。

也存在一個由 persistent_term 模組管理的全域字面值池。

預設情況下,會為所有字面值池(在 BEAM 程式碼和永久項中)保留 1 GB 的虛擬位址空間。可以使用啟動模擬器時的 +MIscs 選項來更改為字面值保留的虛擬位址空間量。

以下範例說明如何將保留給字面值的虛擬位址空間提高到 2 GB(2048 MB):

erl +MIscs 2048

失去共享

Erlang 項可以有共享的子項。以下是一個簡單的範例:

{SubTerm, SubTerm}

在以下情況中,共享的子項不會被保留:

  • 當項被傳送到另一個程序時
  • 當項作為初始程序引數傳遞到 spawn 呼叫中時
  • 當項儲存在 Ets 表格中時

這是一種最佳化。大多數應用程式不會傳送帶有共享子項的訊息。

以下範例顯示如何建立共享的子項:

kilo_byte() ->
    kilo_byte(10, [42]).

kilo_byte(0, Acc) ->
    Acc;
kilo_byte(N, Acc) ->
    kilo_byte(N-1, [Acc|Acc]).

kilo_byte/1 建立一個深度列表。如果呼叫 list_to_binary/1,則可以將深度列表轉換為 1024 位元組的二進制檔案。

1> byte_size(list_to_binary(efficiency_guide:kilo_byte())).
1024

使用 erts_debug:size/1 BIF 可以看出,深度列表只需要 22 個字的堆積空間:

2> erts_debug:size(efficiency_guide:kilo_byte()).
22

使用 erts_debug:flat_size/1 BIF,可以計算如果忽略共享,深度列表的大小。當列表已傳送到另一個程序或儲存在 Ets 表格中時,它的大小將變成這樣:

3> erts_debug:flat_size(efficiency_guide:kilo_byte()).
4094

可以驗證,如果資料插入到 Ets 表格中,共享將會遺失:

4> T = ets:new(tab, []).
#Ref<0.1662103692.2407923716.214181>
5> ets:insert(T, {key,efficiency_guide:kilo_byte()}).
true
6> erts_debug:size(element(2, hd(ets:lookup(T, key)))).
4094
7> erts_debug:flat_size(element(2, hd(ets:lookup(T, key)))).
4094

當資料透過 Ets 表格傳遞後,erts_debug:size/1erts_debug:flat_size/1 會返回相同的值。共享已經遺失。

透過給予 configure 腳本 --enable-sharing-preserving 選項,可以建立一個實驗性的執行時期系統變體,該變體在複製項時將保留共享。

SMP 執行時期系統

Erlang 執行時期系統透過執行多個 Erlang 排程器執行緒(通常,執行緒數量與核心數量相同),來利用多核心或多 CPU 電腦。

為了從多核心電腦獲得效能,你的應用程式大多數時候必須有多個可執行的 Erlang 程序。否則,Erlang 模擬器一次仍然只能執行一個 Erlang 程序。

表面上看起來並行的基準測試,通常是循序的。例如,EStone 基準測試完全是循序的。最常見的「環狀基準測試」實作也是如此;通常只有一個程序是活動的,而其他程序則在 receive 陳述式中等待。