قبل ستة أشهر ، نشر كريس بينر Beating C بـ 80 Lines Of Haskell: Wc . تقول المقدمة:
ويتمثل التحدي في بناء الذكية استنساخ الأمثل أدوات تنفيذ يدويا wc
إلى C في لغتنا رفيع المستوى المفضلة مع برنامج لجمع القمامة - على أن حزقيل ! تبدو بسيطة بما يكفي ، أليس كذلك؟
ذهب كريس على طول الطريق من تنفيذ بسيط باستخدام ByteStrings ، من خلال أحادية ، أحادية مدمجة ، وأخيرًا وصل إلى نسخة متعددة النواة متوازية من ما سبق ، والتي تمكنت من التغلب على شفرة C صغيرة نقية في وقت التشغيل على أربعة نوى.
قبل بضعة أيام على حبري تم نشر ملاحظة أخرى حول نفس الموضوع من 0xd34df00d C Haskell: wc
. أثبت المؤلف إمكانية استخدام اصطلاحي هاسكل وعشرين (20) خطوط للقانون لتنفيذ الخوارزمية، وهو ما يقرب من عشر مرات أسرع من تنفيذ اصطلاحي في لC .
وفي الوقت نفسه ، أنا أدعو إلى استخدام Haskell (في الواقع ، حتى إدريس بسبب أنواعه التابعة) و Coq لإثبات المفاهيم الهامة في قاعدة التعليمات البرمجية لدينا ؛ لكن كل ما يدخل في الإنتاج لا يزال 100٪ Elixir / Erlang / OTP ، بسبب تحملها الخاطئ للخطأ. أردت أن أتأكد من أنني لم أكن كبشًا عنيدًا أحب للتو بناء جملة erlang ، لذلك قررت التحقق مما يمكننا القيام به مع الإكسير الاصطلاحي لنفس المهمة.
أدناه سترى بعض الحيل التي استخدمتها لتسريع التنفيذ تدريجياً ، مما يجعله يصل إلى مستوى مقبول. أنا بالفعل شخص بالغ ولم أعد أؤمن بسانتا كلوز ، ولم أكن أعتز بأي أمل في حدوث معجزة مفادها أن Elixir يمكنه هزيمة اللغات التي يتم تجميعها في كود الآلة. أردت فقط أن أتأكد أننا في النهاية لن نتخلف عن الفائزين في دائرة.
وانا ذاهب الى استخدام بالضبط نفس ملف اختبار كما0xd34df00d. لذلك قمت بتنزيله ولصقته بنفسي عشر مرات ، كما تم توريثه.
% for i in `seq 1 10`; cat part.txt >> test.txt
% du -sh test.txt
1.8G test.txt
على الكمبيوتر المحمول الخاص بي (8 نوى / 16 جيجا رام) ، wc
تستغرق معالجته حوالي 10 ثوانٍ.
time LANG=C LC_ALL=C wc data/test.txt
15000000 44774631 1871822210 data/test.txt
LANG=C LC_ALL=C wc data/test.txt 9,69s user 0,36s system 99% cpu 10,048 total
حسنًا ، دعنا نرى مدى سوء أداء OTP في هذه المهمة .
محاولة ساذجة للغاية
, , , .
@acc %{bc: 0, wc: 1, lc: 0, ns?: 1}
@type acc :: %{
bc: non_neg_integer(),
wc: non_neg_integer(),
lc: non_neg_integer(),
ns?: 0 | 1
}
@spec parse(<<_::_*8>>, acc()) :: acc()
def parse("", acc), do: acc
def parse(
<<?\n, rest::binary>>,
%{bc: bc, wc: wc, lc: lc, ns?: ns}
),
do: parse(rest, %{bc: bc + 1, wc: wc + ns, lc: lc + 1, ns?: 0})
def parse(
<<?\s, rest::binary>>,
%{bc: bc, wc: wc, lc: lc, ns?: ns}
),
do: parse(rest, %{bc: bc + 1, wc: wc + ns, lc: lc, ns?: 0})
def parse(<<_, rest::binary>>, acc),
do: parse(rest, %{acc | bc: acc.bc + 1, ns?: 1})
File.read!/1
, File.stream!/3
:
@spec lazy(binary) :: acc()actually
def lazy(file),
do: file |> File.stream!() |> Enum.reduce(@acc, &parse/2)
@spec greedy(binary) :: acc()
def greedy(file),
do: file |> File.read!() |> parse(@acc)
, , . ; , wc
. , ( μs).
iex|1> :timer.tc fn -> Wc.lazy "data/part.txt" end
#⇒ {16_737094, %{bc: 185682221, lc: 1500000, ns?: 1, wc: 4477464}}
iex|2> :timer.tc fn -> Wc.greedy "data/part.txt" end
, , ? , , .
- -
, ? , . , ?\s
?\n
— — . , , , . , - ( .)
@prehandle 42
@sink @prehandle + 1
@spec parse(<<_::_*8>>, acc()) :: acc()
Enum.each(0..@prehandle, fn i ->
def parse(
<<_::binary-size(unquote(i)), ?\n, rest::binary>>,
%{bc: bc, wc: wc, lc: lc, ns?: ns}
),
do: parse(rest, acc!(unquote(i), bc, wc, lc + 1, ns))
def parse(
<<_::binary-size(unquote(i)), ?\s, rest::binary>>,
%{bc: bc, wc: wc, lc: lc, ns?: ns}
),
do: parse(rest, acc!(unquote(i), bc, wc, lc, ns))
end)
def parse(<<_::binary-size(@sink), rest::binary>>, acc),
do: parse(rest, %{acc | bc: acc.bc + @sink, ns?: 1})
Enum.each(@prehandle..0, fn i ->
def parse(<<_::binary-size(unquote(i))>>, acc),
do: %{acc | bc: acc.bc + unquote(i), ns?: 1}
end)
acc!
— , , .
, , — ? , -, . 130 (43 EOL
, , — :
.
iex|1> :timer.tc fn -> Wc.greedy "data/part.txt" end
#⇒ {2_569929, %{bc: 187182221, lc: 1500000, ns?: 1, wc: 4477464}}
iex|2> :timer.tc fn -> Wc.lazy "data/part.txt" end
, , , wc
(, 2.5 ).
, , , , . , , . , NIF
, , — . , . , Erlang/OTP , , , . - ( ), . , ; , Flow
.
12% ,
, , , (, , - ). mix.exs
( escript
, , ).
def project do
[
app: :wc,
version: "0.1.0",
elixir: "~> 1.9",
start_permanent: Mix.env() == :prod,
escript: [main_module: Wc.Main],
deps: [{:flow, "~> 1.0"}]
]
end
.
@chunk 1_000_000
@spec flowy(binary()) :: acc()
def flowy(file) do
file
|> File.stream!([], @chunk)
|> Flow.from_enumerable()
|> Flow.partition()
|> Flow.reduce(fn -> @acc end, &parse/2)
|> Enum.to_list()
end
( ) , , 1M
. .
!
iex|1> :timer.tc fn -> Wc.flowy "data/part.txt" end
#⇒ {0_752568, %{bc: 187182221, lc: 1500000, ns?: 1, wc: 4477464}}
iex|2> :timer.tc fn -> Wc.flowy "data/test.txt" end
! , (1.8
). , , wc
, , , , . , , : , , . mix escript.build
, , .
time LANG=C LC_ALL=C wc data/test.txt
15000000 44774631 1871822210 data/test.txt
LANG=C LC_ALL=C wc data/test.txt 9,71s user 0,39s system 99% cpu 10,104 total
time ./wc data/test.txt
15000000 44774631 1871822210 data/test.txt
./wc data/test.txt 41,72s user 2,31s system 706% cpu 6,355 total
.
( main
, escript
) — . , , — 20 ,
!