Profiling

يوفر نموذج Profile أدوات لمساعدة المطورين على تحسين أداء شيفرتهم. عند استخدامه، يقوم بأخذ قياسات على الشيفرة التي تعمل، وينتج مخرجات تساعدك على فهم مقدار الوقت المستغرق في سطر (أسطر) فردية. الاستخدام الأكثر شيوعًا هو تحديد "اختناقات" كأهداف للتحسين.

Profile ينفذ ما يُعرف بـ "العينة" أو statistical profiler. يعمل من خلال أخذ تتبع خلفي بشكل دوري أثناء تنفيذ أي مهمة. كل تتبع خلفي يلتقط الوظيفة الحالية قيد التشغيل ورقم السطر، بالإضافة إلى السلسلة الكاملة من استدعاءات الوظائف التي أدت إلى هذا السطر، ومن ثم يُعتبر "لقطة" للحالة الحالية للتنفيذ.

إذا كانت الكثير من وقت التشغيل الخاص بك تُقضى في تنفيذ سطر معين من الشيفرة، فسوف يظهر هذا السطر بشكل متكرر في مجموعة جميع تتبعات الأخطاء. بعبارة أخرى، فإن "تكلفة" سطر معين - أو في الواقع، تكلفة تسلسل استدعاءات الدوال حتى بما في ذلك هذا السطر - تتناسب مع مدى تكرار ظهوره في مجموعة جميع تتبعات الأخطاء.

لا يوفر ملف التعريف العيني تغطية كاملة سطرًا بسطر، لأن تتبع الخلفيات يحدث على فترات (بشكل افتراضي، 1 مللي ثانية على أنظمة Unix و10 مللي ثانية على Windows، على الرغم من أن الجدولة الفعلية تخضع لحمولة نظام التشغيل). علاوة على ذلك، كما تم مناقشته أدناه، نظرًا لأن العينات تُجمع في مجموعة فرعية نادرة من جميع نقاط التنفيذ، فإن البيانات التي تجمعها أداة التعريف العيني تخضع للضوضاء الإحصائية.

على الرغم من هذه القيود، فإن أدوات تحليل العينة تتمتع بقوة كبيرة:

  • لا تحتاج إلى إجراء أي تعديلات على الشيفرة الخاصة بك لأخذ قياسات التوقيت.
  • يمكنه التعمق في الشيفرة الأساسية لجوليا وحتى (اختياريًا) في مكتبات C و Fortran.
  • من خلال تشغيل "بشكل غير متكرر" هناك القليل جداً من عبء الأداء؛ أثناء التقييم، يمكن أن يعمل كودك بسرعة قريبة من السرعة الأصلية.

لذلك، يُوصى بأن تحاول استخدام أداة تحليل العينة المدمجة قبل التفكير في أي بدائل.

Basic usage

دعنا نعمل مع حالة اختبار بسيطة:

julia> function myfunc()
           A = rand(200, 200, 400)
           maximum(A)
       end

من الجيد أن تقوم بتشغيل الكود الذي تنوي تقييم أدائه مرة واحدة على الأقل (ما لم تكن ترغب في تقييم مترجم JIT الخاص بـ Julia):

julia> myfunc() # run once to force compilation

الآن نحن مستعدون لتحديد ملف تعريف هذه الدالة:

julia> using Profile

julia> @profile myfunc()

لرؤية نتائج التحليل، هناك العديد من المتصفحات الرسومية. واحدة من "عائلات" المرئيات تعتمد على FlameGraphs.jl، مع كل عضو في العائلة يقدم واجهة مستخدم مختلفة:

  • VS Code هو IDE كامل مع دعم مدمج لتصور الملف الشخصي
  • ProfileView.jl هو مُصوِّر مستقل يعتمد على GTK
  • ProfileVega.jl يستخدم VegaLight ويتكامل بشكل جيد مع دفاتر Jupyter
  • StatProfilerHTML.jl ينتج HTML ويقدم بعض الملخصات الإضافية، كما أنه يتكامل بشكل جيد مع دفاتر Jupyter
  • ProfileSVG.jl يعرض SVG
  • PProf.jl يخدم موقعًا محليًا لفحص الرسوم البيانية، والرسوم النارية، والمزيد
  • ProfileCanvas.jl هو واجهة عرض ملف تعريف تعتمد على HTML canvas، تُستخدم بواسطة Julia VS Code extension، ولكن يمكنها أيضًا إنشاء ملفات HTML تفاعلية.

نهج مستقل تمامًا لتصور الملف الشخصي هو PProf.jl، والذي يستخدم أداة pprof الخارجية.

هنا، على الرغم من ذلك، سنستخدم العرض القائم على النص الذي يأتي مع المكتبة القياسية:

julia> Profile.print()
80 ./event.jl:73; (::Base.REPL.##1#2{Base.REPL.REPLBackend})()
 80 ./REPL.jl:97; macro expansion
  80 ./REPL.jl:66; eval_user_input(::Any, ::Base.REPL.REPLBackend)
   80 ./boot.jl:235; eval(::Module, ::Any)
    80 ./<missing>:?; anonymous
     80 ./profile.jl:23; macro expansion
      52 ./REPL[1]:2; myfunc()
       38 ./random.jl:431; rand!(::MersenneTwister, ::Array{Float64,3}, ::Int64, ::Type{B...
        38 ./dSFMT.jl:84; dsfmt_fill_array_close_open!(::Base.dSFMT.DSFMT_state, ::Ptr{F...
       14 ./random.jl:278; rand
        14 ./random.jl:277; rand
         14 ./random.jl:366; rand
          14 ./random.jl:369; rand
      28 ./REPL[1]:3; myfunc()
       28 ./reduce.jl:270; _mapreduce(::Base.#identity, ::Base.#scalarmax, ::IndexLinear,...
        3  ./reduce.jl:426; mapreduce_impl(::Base.#identity, ::Base.#scalarmax, ::Array{F...
        25 ./reduce.jl:428; mapreduce_impl(::Base.#identity, ::Base.#scalarmax, ::Array{F...

كل سطر من هذا العرض يمثل نقطة معينة (رقم السطر) في الشيفرة. يتم استخدام المسافة البادئة للإشارة إلى تسلسل استدعاءات الدوال المتداخلة، حيث تكون الأسطر الأكثر تداخلاً أعمق في تسلسل الاستدعاءات. في كل سطر، يكون "الحقل" الأول هو عدد تتبعات الأخطاء (عينات) المأخوذة في هذا السطر أو في أي دوال تم تنفيذها بواسطة هذا السطر. الحقل الثاني هو اسم الملف ورقم السطر والحقل الثالث هو اسم الدالة. لاحظ أن أرقام الأسطر المحددة قد تتغير مع تغير شيفرة جوليا؛ إذا كنت ترغب في المتابعة، فمن الأفضل تشغيل هذا المثال بنفسك.

في هذا المثال، يمكننا أن نرى أن الدالة الرئيسية التي تم استدعاؤها موجودة في الملف event.jl. هذه هي الدالة التي تشغل REPL عند تشغيلك لجوليا. إذا قمت بفحص السطر 97 من REPL.jl، سترى أن هذه هي النقطة التي يتم فيها استدعاء الدالة eval_user_input(). هذه هي الدالة التي تقيم ما تكتبه في REPL، ونظرًا لأننا نعمل بشكل تفاعلي، تم استدعاء هذه الدوال عندما أدخلنا @profile myfunc(). السطر التالي يعكس الإجراءات المتخذة في الماكرو @profile.

السطر الأول يُظهر أنه تم أخذ 80 تتبعًا عند السطر 73 من event.jl، لكن ليس من الصحيح أن هذا السطر كان "مكلفًا" بمفرده: السطر الثالث يكشف أن جميع هذه التتبعيات الـ 80 تم تفعيلها فعليًا داخل استدعائه لـ eval_user_input، وهكذا. للعثور على العمليات التي تأخذ الوقت فعليًا، نحتاج إلى النظر أعمق في سلسلة الاستدعاءات.

السطر "المهم" الأول في هذا الإخراج هو هذا:

52 ./REPL[1]:2; myfunc()

REPL يشير إلى حقيقة أننا عرفنا myfunc في REPL، بدلاً من وضعه في ملف؛ إذا كنا قد استخدمنا ملفًا، فإن هذا سيظهر اسم الملف. الـ [1] يظهر أن الدالة myfunc كانت أول تعبير تم تقييمه في جلسة REPL هذه. تحتوي السطر 2 من myfunc() على استدعاء لـ rand، وكان هناك 52 (من أصل 80) تتبعات حدثت في هذا السطر. أدناه، يمكنك رؤية استدعاء لـ dsfmt_fill_array_close_open! داخل dSFMT.jl.

قليلاً أسفل، سترى:

28 ./REPL[1]:3; myfunc()

السطر 3 من myfunc يحتوي على استدعاء لـ maximum، وكان هناك 28 (من أصل 80) تتبعات تم أخذها هنا. أدناه، يمكنك رؤية الأماكن المحددة في base/reduce.jl التي تقوم بتنفيذ العمليات المستهلكة للوقت في دالة maximum لهذا النوع من بيانات الإدخال.

بشكل عام، يمكننا أن نستنتج بشكل مؤقت أن توليد الأرقام العشوائية يكلف تقريبًا ضعف تكلفة العثور على العنصر الأقصى. يمكننا زيادة ثقتنا في هذه النتيجة من خلال جمع المزيد من العينات:

julia> @profile (for i = 1:100; myfunc(); end)

julia> Profile.print()
[....]
 3821 ./REPL[1]:2; myfunc()
  3511 ./random.jl:431; rand!(::MersenneTwister, ::Array{Float64,3}, ::Int64, ::Type...
   3511 ./dSFMT.jl:84; dsfmt_fill_array_close_open!(::Base.dSFMT.DSFMT_state, ::Ptr...
  310  ./random.jl:278; rand
   [....]
 2893 ./REPL[1]:3; myfunc()
  2893 ./reduce.jl:270; _mapreduce(::Base.#identity, ::Base.#scalarmax, ::IndexLinea...
   [....]

بشكل عام، إذا كان لديك N عينات تم جمعها على خط، يمكنك توقع وجود عدم يقين على مستوى sqrt(N) (باستثناء مصادر الضوضاء الأخرى، مثل مدى انشغال الكمبيوتر بمهام أخرى). الاستثناء الرئيسي لهذه القاعدة هو جمع القمامة، الذي يعمل بشكل غير متكرر ولكنه يميل إلى أن يكون مكلفًا جدًا. (نظرًا لأن جامع القمامة في جوليا مكتوب بلغة C، يمكن اكتشاف مثل هذه الأحداث باستخدام وضع الإخراج C=true الموضح أدناه، أو عن طريق استخدام ProfileView.jl.)

هذا يوضح تفريغ "الشجرة" الافتراضي؛ بديل آخر هو تفريغ "المسطح"، الذي يجمع العدّات بشكل مستقل عن تداخلها:

julia> Profile.print(format=:flat)
 Count File          Line Function
  6714 ./<missing>     -1 anonymous
  6714 ./REPL.jl       66 eval_user_input(::Any, ::Base.REPL.REPLBackend)
  6714 ./REPL.jl       97 macro expansion
  3821 ./REPL[1]        2 myfunc()
  2893 ./REPL[1]        3 myfunc()
  6714 ./REPL[7]        1 macro expansion
  6714 ./boot.jl      235 eval(::Module, ::Any)
  3511 ./dSFMT.jl      84 dsfmt_fill_array_close_open!(::Base.dSFMT.DSFMT_s...
  6714 ./event.jl      73 (::Base.REPL.##1#2{Base.REPL.REPLBackend})()
  6714 ./profile.jl    23 macro expansion
  3511 ./random.jl    431 rand!(::MersenneTwister, ::Array{Float64,3}, ::In...
   310 ./random.jl    277 rand
   310 ./random.jl    278 rand
   310 ./random.jl    366 rand
   310 ./random.jl    369 rand
  2893 ./reduce.jl    270 _mapreduce(::Base.#identity, ::Base.#scalarmax, :...
     5 ./reduce.jl    420 mapreduce_impl(::Base.#identity, ::Base.#scalarma...
   253 ./reduce.jl    426 mapreduce_impl(::Base.#identity, ::Base.#scalarma...
  2592 ./reduce.jl    428 mapreduce_impl(::Base.#identity, ::Base.#scalarma...
    43 ./reduce.jl    429 mapreduce_impl(::Base.#identity, ::Base.#scalarma...

إذا كان لديك كود يحتوي على تكرار، فإن نقطة قد تكون مربكة هي أن سطرًا في دالة "طفل" يمكن أن يجمع المزيد من العدادات مما هو موجود من إجمالي تتبعات العودة. اعتبر تعريفات الدوال التالية:

dumbsum(n::Integer) = n == 1 ? 1 : 1 + dumbsum(n-1)
dumbsum3() = dumbsum(3)

إذا كنت ستقوم بملف تعريف dumbsum3، وتم أخذ تتبع للعودة أثناء تنفيذها dumbsum(1)، فسيبدو تتبع العودة كما يلي:

dumbsum3
    dumbsum(3)
        dumbsum(2)
            dumbsum(1)

نتيجة لذلك، يحصل هذا الدالة الفرعية على 3 عدادات، على الرغم من أن الوالد يحصل على واحدة فقط. تمثل "الشجرة" هذا بشكل أوضح بكثير، ولهذا السبب (من بين أسباب أخرى) من المحتمل أن تكون هذه هي الطريقة الأكثر فائدة لرؤية النتائج.

Accumulation and clearing

تتراكم النتائج من @profile في ذاكرة مؤقتة؛ إذا قمت بتشغيل عدة قطع من الشيفرة تحت 4d61726b646f776e2e436f64652822222c20224070726f66696c652229_40726566، فإن Profile.print() ستظهر لك النتائج المجمعة. يمكن أن يكون هذا مفيدًا جدًا، ولكن في بعض الأحيان تريد البدء من جديد؛ يمكنك القيام بذلك باستخدام Profile.clear().

Options for controlling the display of profile results

Profile.print يحتوي على المزيد من الخيارات مما وصفناه حتى الآن. دعنا نرى الإعلان الكامل:

function print(io::IO = stdout, data = fetch(); kwargs...)

دعونا نناقش أولاً الحجج الموضعية، ثم الحجج بالكلمات المفتاحية:

  • io – يتيح لك حفظ النتائج في ذاكرة مؤقتة، مثل ملف، ولكن الإعداد الافتراضي هو الطباعة إلى stdout (وحدة التحكم).

  • data – يحتوي على البيانات التي تريد تحليلها؛ بشكل افتراضي يتم الحصول عليها من Profile.fetch()، والذي يستخرج تتبعات الأخطاء من ذاكرة مؤقتة مُعدة مسبقًا. على سبيل المثال، إذا كنت تريد تحليل المحلل، يمكنك أن تقول:

    data = copy(Profile.fetch())
    Profile.clear()
    @profile Profile.print(stdout, data) # Prints the previous results
    Profile.print()                      # Prints results from Profile.print()

يمكن أن تكون وسائط الكلمات الرئيسية أي مجموعة من:

  • format – تم تقديمه أعلاه، يحدد ما إذا كانت تتبع الأخطاء مطبوعة مع (افتراضي، :tree) أو بدون (:flat) تباين يشير إلى هيكل الشجرة.
  • C – إذا كانت true، يتم عرض تتبعات الأخطاء من كود C و Fortran (عادةً ما يتم استبعادها). حاول تشغيل المثال التمهيدي باستخدام Profile.print(C = true). يمكن أن يكون هذا مفيدًا للغاية في تحديد ما إذا كان كود جوليا أو كود C هو الذي يسبب عنق الزجاجة؛ كما أن تعيين C = true يحسن أيضًا من قابلية تفسير التداخل، على حساب زيادة طول تفريغ الملف الشخصي.
  • combine – تحتوي بعض أسطر الشيفرة على عمليات متعددة؛ على سبيل المثال، s += A[i] تحتوي على كل من مرجع المصفوفة (A[i]) وعملية الجمع. هذه تتوافق مع أسطر مختلفة في الشيفرة الآلية المولدة، وبالتالي قد يتم التقاط عنوانين أو أكثر مختلفين خلال تتبعات الأخطاء على هذا السطر. combine = true تجمعها معًا، ومن المحتمل أن يكون هذا ما تريده عادةً، ولكن يمكنك توليد مخرجات بشكل منفصل لكل مؤشر تعليمات فريد مع combine = false.
  • maxdepth – يحدد الإطارات على عمق أعلى من maxdepth في تنسيق :tree.
  • sortedby – يتحكم في الترتيب في تنسيق :flat. يقوم :filefuncline (افتراضي) بالفرز حسب سطر المصدر، بينما يقوم :count بالفرز حسب عدد العينات المجمعة.
  • noisefloor – يحدد الإطارات التي تقل عن مستوى الضوضاء الاستدلالي للعينة (ينطبق فقط على التنسيق :tree). قيمة مقترحة لتجربتها هي 2.0 (القيمة الافتراضية هي 0). هذا المعامل يخفي العينات التي يكون فيها n <= noisefloor * √N، حيث n هو عدد العينات في هذا السطر، و N هو عدد العينات للمتصل.
  • mincount – يحدد الإطارات التي تحتوي على أقل من mincount حدوث.

File/function names are sometimes truncated (with ...), and indentation is truncated with a +n at the beginning, where n is the number of extra spaces that would have been inserted, had there been room. If you want a complete profile of deeply-nested code, often a good idea is to save to a file using a wide displaysize in an IOContext:

open("/tmp/prof.txt", "w") do s
    Profile.print(IOContext(s, :displaysize => (24, 500)))
end

Configuration

@profile فقط يجمع تتبعات الأخطاء، وتحدث التحليلات عندما تستدعي Profile.print(). بالنسبة لحساب طويل الأمد، من الممكن تمامًا أن يتم ملء المخزن المخصص لتخزين تتبعات الأخطاء. إذا حدث ذلك، تتوقف تتبعات الأخطاء لكن حسابك يستمر. ونتيجة لذلك، قد تفوت بعض بيانات التوصيف المهمة (ستتلقى تحذيرًا عندما يحدث ذلك).

يمكنك الحصول على المعلمات ذات الصلة وتكوينها بهذه الطريقة:

Profile.init() # returns the current settings
Profile.init(n = 10^7, delay = 0.01)

n هو العدد الإجمالي لمؤشرات التعليمات التي يمكنك تخزينها، مع قيمة افتراضية تبلغ 10^6. إذا كانت تتبع التعليمات النموذجي لديك هو 20 مؤشر تعليمات، فيمكنك جمع 50000 تتبع، مما يشير إلى عدم اليقين الإحصائي أقل من 1%. قد يكون هذا جيدًا بما يكفي لمعظم التطبيقات.

نتيجة لذلك، من المرجح أن تحتاج إلى تعديل delay، المعبر عنه بالثواني، والذي يحدد مقدار الوقت الذي تحصل عليه جوليا بين اللقطات لأداء الحسابات المطلوبة. قد لا تحتاج الوظيفة التي تستغرق وقتًا طويلاً إلى تتبعات متكررة. الإعداد الافتراضي هو delay = 0.001. بالطبع، يمكنك تقليل التأخير وكذلك زيادته؛ ومع ذلك، فإن عبء العمل الناتج عن التقييم ينمو بمجرد أن يصبح التأخير مشابهًا للوقت اللازم لأخذ تتبع (~30 ميكروثانية على جهاز الكمبيوتر المحمول للمؤلف).

Memory allocation analysis

أحد أكثر التقنيات شيوعًا لتحسين الأداء هو تقليل تخصيص الذاكرة. توفر جوليا العديد من الأدوات لقياس ذلك:

@time

يمكن قياس إجمالي مقدار التخصيص باستخدام @time، @allocated و @allocations، ويمكن غالبًا استنتاج الأسطر المحددة التي تؤدي إلى التخصيص من خلال التحليل عبر تكلفة جمع القمامة التي تتكبدها هذه الأسطر. ومع ذلك، في بعض الأحيان يكون من الأكثر كفاءة قياس مقدار الذاكرة المخصصة مباشرةً بواسطة كل سطر من التعليمات البرمجية.

GC Logging

بينما يسجل @time إحصائيات عالية المستوى حول استخدام الذاكرة وجمع القمامة على مدار تقييم تعبير ما، يمكن أن يكون من المفيد تسجيل كل حدث لجمع القمامة، للحصول على فكرة بديهية عن مدى تكرار تشغيل جامع القمامة، ومدة تشغيله في كل مرة، وكمية القمامة التي يجمعها في كل مرة. يمكن تفعيل ذلك باستخدام GC.enable_logging(true)، مما يتسبب في تسجيل جوليا إلى stderr في كل مرة يحدث فيها جمع للقمامة.

Allocation Profiler

Julia 1.8

تتطلب هذه الوظيفة على الأقل جوليا 1.8.

يسجل محلل التخصيص تتبع المكدس، النوع، وحجم كل تخصيص أثناء تشغيله. يمكن استدعاؤه باستخدام Profile.Allocs.@profile.

تُرجع هذه المعلومات حول التخصيصات كمصفوفة من كائنات Alloc، ملفوفة في كائن AllocResults. أفضل طريقة لتصور هذه المعلومات حاليًا هي باستخدام الحزم PProf.jl و ProfileCanvas.jl، والتي يمكن أن تصور مكدسات الاستدعاء التي تقوم بأكبر عدد من التخصيصات.

يحتوي ملف تعريف التخصيص على عبء كبير، لذا يمكن تمرير وسيط sample_rate لتسريعه عن طريق جعله يتخطى بعض التخصيصات. تمرير sample_rate=1.0 سيجعله يسجل كل شيء (وهو بطيء)؛ sample_rate=0.1 سيسجل فقط 10% من التخصيصات (أسرع)، وهكذا.

Julia 1.11

لم تكن الإصدارات القديمة من جوليا قادرة على التقاط الأنواع في جميع الحالات. في الإصدارات القديمة من جوليا، إذا رأيت تخصيصًا من النوع Profile.Allocs.UnknownType، فهذا يعني أن أداة التحليل لا تعرف نوع الكائن الذي تم تخصيصه. حدث هذا بشكل رئيسي عندما كان التخصيص يأتي من الشيفرة المولدة التي ينتجها المترجم. راجع issue #43688 لمزيد من المعلومات.

منذ Julia 1.11، يجب أن تحتوي جميع التخصيصات على نوع مُبلغ عنه.

لمزيد من التفاصيل حول كيفية استخدام هذه الأداة، يرجى الاطلاع على الحديث التالي من JuliaCon 2022: https://www.youtube.com/watch?v=BFvpwC8hEWQ

Allocation Profiler Example

في هذا المثال البسيط، نستخدم PProf لتصور ملف التخصيص. يمكنك استخدام أداة تصور أخرى بدلاً من ذلك. نجمع الملف (مع تحديد معدل العينة)، ثم نقوم بتصوره.

using Profile, PProf
Profile.Allocs.clear()
Profile.Allocs.@profile sample_rate=0.0001 my_function()
PProf.Allocs.pprof()

هنا مثال أكثر تفصيلاً، يوضح كيف يمكننا ضبط معدل العينة. عدد جيد من العينات الذي يجب أن نستهدفه هو حوالي 1 - 10 آلاف. إذا كان العدد كبيرًا جدًا، يمكن أن يتعرض مرئي الملف الشخصي للإرهاق، وسيكون التقييم بطيئًا. إذا كان العدد قليلًا جدًا، فلن تحصل على عينة تمثيلية.

julia> import Profile

julia> @time my_function()  # Estimate allocations from a (second-run) of the function
  0.110018 seconds (1.50 M allocations: 58.725 MiB, 17.17% gc time)
500000

julia> Profile.Allocs.clear()

julia> Profile.Allocs.@profile sample_rate=0.001 begin   # 1.5 M * 0.001 = ~1.5K allocs.
           my_function()
       end
500000

julia> prof = Profile.Allocs.fetch();  # If you want, you can also manually inspect the results.

julia> length(prof.allocs)  # Confirm we have expected number of allocations.
1515

julia> using PProf  # Now, visualize with an external tool, like PProf or ProfileCanvas.

julia> PProf.Allocs.pprof(prof; from_c=false)  # You can optionally pass in a previously fetched profile result.
Analyzing 1515 allocation samples... 100%|████████████████████████████████| Time: 0:00:00
Main binary filename not available.
Serving web UI on http://localhost:62261
"alloc-profile.pb.gz"

ثم يمكنك عرض الملف الشخصي عن طريق الانتقال إلى http://localhost:62261، والملف الشخصي محفوظ على القرص. راجع حزمة PProf لمزيد من الخيارات.

Allocation Profiling Tips

كما هو مذكور أعلاه، استهدف حوالي 1-10 آلاف عينة في ملفك الشخصي.

لاحظ أننا نقوم بأخذ عينات بشكل موحد في فضاء جميع التخصيصات، ولا نقوم بوزن عيناتنا حسب حجم التخصيص. لذا قد لا تعطي ملف تخصيص معين تمثيلاً تمثيليًا لمكان تخصيص معظم البايتات في برنامجك، ما لم تكن قد قمت بتعيين sample_rate=1.

يمكن أن تأتي التخصيصات من المستخدمين الذين يقومون بإنشاء كائنات مباشرة، ولكن يمكن أن تأتي أيضًا من داخل وقت التشغيل أو يتم إدراجها في الشيفرة المترجمة للتعامل مع عدم استقرار النوع. يمكن أن يكون النظر إلى عرض "الشيفرة المصدرية" مفيدًا لعزلها، ثم يمكن أن تكون أدوات خارجية أخرى مثل Cthulhu.jl مفيدة لتحديد سبب التخصيص.

Allocation Profile Visualization Tools

هناك العديد من أدوات تصور التحليل الآن التي يمكنها جميعًا عرض ملفات تعريف التخصيص. إليك قائمة صغيرة ببعض من أبرزها التي نعرفها:

  • PProf.jl

  • ProfileCanvas.jl

  • VSCode's built-in profile visualizer (@profview_allocs) [docs needed]

  • عرض النتائج مباشرة في REPL

    • يمكنك فحص النتائج في REPL عبر Profile.Allocs.fetch()، لرؤية تتبع المكدس ونوع كل تخصيص.

Line-by-Line Allocation Tracking

طريقة بديلة لقياس التخصيصات هي بدء جوليا مع خيار سطر الأوامر --track-allocation=<setting>، حيث يمكنك اختيار none (الإعداد الافتراضي، عدم قياس التخصيص)، user (قياس تخصيص الذاكرة في كل مكان باستثناء كود جوليا الأساسي)، أو all (قياس تخصيص الذاكرة في كل سطر من كود جوليا). يتم قياس التخصيص لكل سطر من الكود المترجم. عند إنهاء جوليا، يتم كتابة النتائج التراكمية إلى ملفات نصية مع .mem مضافة بعد اسم الملف، وتقع في نفس الدليل الذي يوجد فيه ملف المصدر. يسرد كل سطر العدد الإجمالي للبايتات المخصصة. يحتوي Coverage package على بعض أدوات التحليل الأساسية، على سبيل المثال لترتيب الأسطر حسب عدد البايتات المخصصة.

في تفسير النتائج، هناك بعض التفاصيل المهمة. تحت إعداد user، ستظهر السطر الأول من أي دالة يتم استدعاؤها مباشرة من REPL تخصيصًا بسبب الأحداث التي تحدث في كود REPL نفسه. والأهم من ذلك، أن تجميع JIT يضيف أيضًا إلى أعداد التخصيص، لأن جزءًا كبيرًا من مترجم جوليا مكتوب بلغة جوليا (وغالبًا ما يتطلب التجميع تخصيص الذاكرة). الإجراء الموصى به هو إجبار التجميع عن طريق تنفيذ جميع الأوامر التي تريد تحليلها، ثم استدعاء Profile.clear_malloc_data() لإعادة تعيين جميع عدادات التخصيص. أخيرًا، نفذ الأوامر المطلوبة واخرج من جوليا لتحفيز إنشاء ملفات .mem.

Note

--track-allocation يغير توليد الشيفرة لتسجيل التخصيصات، وبالتالي قد تكون التخصيصات مختلفة عما يحدث بدون الخيار. نوصي باستخدام allocation profiler بدلاً من ذلك.

External Profiling

حاليًا تدعم جوليا Intel VTune و OProfile و perf كأدوات خارجية للتعقب.

اعتمادًا على الأداة التي تختارها، قم بتجميع مع USE_INTEL_JITEVENTS، USE_OPROFILE_JITEVENTS و USE_PERF_JITEVENTS مضبوطة على 1 في Make.user. يتم دعم عدة علامات.

قبل تشغيل جوليا، قم بتعيين متغير البيئة ENABLE_JITPROFILING إلى 1.

الآن لديك العديد من الطرق لاستخدام تلك الأدوات! على سبيل المثال، مع OProfile يمكنك تجربة تسجيل بسيط:

>ENABLE_JITPROFILING=1 sudo operf -Vdebug ./julia test/fastmath.jl
>opreport -l `which ./julia`

أو بشكل مشابه مع perf :

$ ENABLE_JITPROFILING=1 perf record -o /tmp/perf.data --call-graph dwarf -k 1 ./julia /test/fastmath.jl
$ perf inject --jit --input /tmp/perf.data --output /tmp/perf-jit.data
$ perf report --call-graph -G -i /tmp/perf-jit.data

هناك العديد من الأشياء المثيرة للاهتمام التي يمكنك قياسها حول برنامجك، للحصول على قائمة شاملة يرجى قراءة Linux perf examples page.

تذكر أن perf يحفظ لكل تنفيذ ملف perf.data الذي، حتى بالنسبة للبرامج الصغيرة، يمكن أن يصبح كبيرًا جدًا. أيضًا، يقوم وحدة LLVM الخاصة بـ perf بحفظ كائنات التصحيح مؤقتًا في ~/.debug/jit، تذكر أن تقوم بتنظيف هذا المجلد بشكل متكرر.