it-swarm.asia

رمز C++ لاختبار التخمين Collatz أسرع من التجميع المكتوبة بخط اليد - لماذا؟

كتبت هذين الحلين لـ Project Euler Q14 ، in Assembly and C++. هم نفس نهج القوة الغاشمة متطابقة لاختبار التخمين Collatz . تم تجميع حل الجمعية مع

nasm -felf64 p14.asm && gcc p14.o -o p14

تم تجميع C++ مع

g++ p14.cpp -o p14

التجميع ، p14.asm

section .data
    fmt db "%d", 10, 0

global main
extern printf

section .text

main:
    mov rcx, 1000000
    xor rdi, rdi        ; max i
    xor rsi, rsi        ; i

l1:
    dec rcx
    xor r10, r10        ; count
    mov rax, rcx

l2:
    test rax, 1
    jpe even

    mov rbx, 3
    mul rbx
    inc rax
    jmp c1

even:
    mov rbx, 2
    xor rdx, rdx
    div rbx

c1:
    inc r10
    cmp rax, 1
    jne l2

    cmp rdi, r10
    cmovl rdi, r10
    cmovl rsi, rcx

    cmp rcx, 2
    jne l1

    mov rdi, fmt
    xor rax, rax
    call printf
    ret

C++ ، p14.cpp

#include <iostream>

using namespace std;

int sequence(long n) {
    int count = 1;
    while (n != 1) {
        if (n % 2 == 0)
            n /= 2;
        else
            n = n*3 + 1;

        ++count;
    }

    return count;
}

int main() {
    int max = 0, maxi;
    for (int i = 999999; i > 0; --i) {
        int s = sequence(i);
        if (s > max) {
            max = s;
            maxi = i;
        }
    }

    cout << maxi << endl;
}

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

يحتوي رمز C++ على معامل لكل مصطلح وقسم لكل فصل ، حيث التجميع هو تقسيم واحد فقط لكل فصل.

لكن الجمعية تأخذ في المتوسط ​​1 ثانية أطول من حل C++. لماذا هذا؟ أنا أسأل بدافع الفضول.

أوقات التنفيذ

نظامي: 64 بت لينكس على 1.4 جيجا هرتز Intel Celeron 2955U (Haswell microarchitecture).

778
jeffer son

الادعاء بأن برنامج التحويل البرمجي C++ يمكنه إنتاج كود أفضل من مبرمج لغة التجميع المختص يعد خطأً سيئًا للغاية. وخاصة في هذه الحالة. يمكن للبشر دائمًا أن يجعل الكود أفضل من المحول البرمجي ، وهذا الموقف بالذات هو توضيح جيد لهذه المطالبة.

اختلاف التوقيت الذي تراه هو أن رمز التجميع في السؤال بعيد جدًا عن الأمثل في الحلقات الداخلية.

(الكود أدناه هو 32 بت ، ولكن يمكن تحويله بسهولة إلى 64 بت)

على سبيل المثال ، يمكن تحسين وظيفة التسلسل إلى 5 تعليمات فقط:

    .seq:
        inc     esi                 ; counter
        lea     edx, [3*eax+1]      ; edx = 3*n+1
        shr     eax, 1              ; eax = n/2
        cmovc   eax, edx            ; if CF eax = edx
        jnz     .seq                ; jmp if n<>1

يشبه الرمز بالكامل:

include "%lib%/freshlib.inc"
@BinaryType console, compact
options.DebugMode = 1
include "%lib%/freshlib.asm"

start:
        InitializeAll
        mov ecx, 999999
        xor edi, edi        ; max
        xor ebx, ebx        ; max i

    .main_loop:

        xor     esi, esi
        mov     eax, ecx

    .seq:
        inc     esi                 ; counter
        lea     edx, [3*eax+1]      ; edx = 3*n+1
        shr     eax, 1              ; eax = n/2
        cmovc   eax, edx            ; if CF eax = edx
        jnz     .seq                ; jmp if n<>1

        cmp     edi, esi
        cmovb   edi, esi
        cmovb   ebx, ecx

        dec     ecx
        jnz     .main_loop

        OutputValue "Max sequence: ", edi, 10, -1
        OutputValue "Max index: ", ebx, 10, -1

        FinalizeAll
        stdcall TerminateAll, 0

من أجل ترجمة هذا الرمز ، FreshLib هو مطلوب.

في الاختبارات التي أجريتها (معالج AMD A4-1200 بسرعة 1 جيجاهرتز) ، يكون الرمز أعلاه أسرع أربع مرات تقريبًا من رمز C++ من السؤال (عند التحويل البرمجي باستخدام -O0: 430 مللي ثانية مقابل 1900 مللي ثانية) ، وأسرع أكثر من مرتين ( 430 مللي ثانية مقابل 830 مللي ثانية) عند ترجمة رمز C++ مع -O3.

إخراج كلا البرنامجين هو نفسه: أقصى تسلسل = 525 على i = 837799.

95
johnfound

لمزيد من الأداء: يلاحظ تغيير بسيط أنه بعد n = 3n + 1 ، ستكون n متساوية ، بحيث يمكنك القسمة على 2 على الفور. ولن تكون 1 ، لذلك لن تحتاج إلى اختبارها. لذلك يمكنك توفير عدد قليل من البيانات إذا والكتابة:

while (n % 2 == 0) n /= 2;
if (n > 1) for (;;) {
    n = (3*n + 1) / 2;
    if (n % 2 == 0) {
        do n /= 2; while (n % 2 == 0);
        if (n == 1) break;
    }
}

إليك a big win: إذا نظرت إلى أدنى 8 بتات من n ، فكل الخطوات إلى أن تنقسم على 2 ثماني مرات يتم تحديدها بالكامل بواسطة تلك البتات الثمانية. على سبيل المثال ، إذا كانت آخر ثمانية بتات هي 0x01 ، فهذا يعني أن الرقم ثنائي هو ؟؟؟ 0000 0001 ثم الخطوات التالية هي:

3n+1 -> ???? 0000 0100
/ 2  -> ???? ?000 0010
/ 2  -> ???? ??00 0001
3n+1 -> ???? ??00 0100
/ 2  -> ???? ???0 0010
/ 2  -> ???? ???? 0001
3n+1 -> ???? ???? 0100
/ 2  -> ???? ???? ?010
/ 2  -> ???? ???? ??01
3n+1 -> ???? ???? ??00
/ 2  -> ???? ???? ???0
/ 2  -> ???? ???? ????

لذلك يمكن توقع كل هذه الخطوات ، واستبدال 256 كيلو + 1 بـ 81 كيلو + 1. سيحدث شيء مشابه لجميع المجموعات. حتى تتمكن من إنشاء حلقة مع بيان التبديل الكبير:

k = n / 256;
m = n % 256;

switch (m) {
    case 0: n = 1 * k + 0; break;
    case 1: n = 81 * k + 1; break; 
    case 2: n = 81 * k + 1; break; 
    ...
    case 155: n = 729 * k + 425; break;
    ...
}

قم بتشغيل الحلقة حتى n ≤ 128 ، لأنه في هذه المرحلة يمكن أن تصبح n 1 مع أقل من ثمانية أقسام بحلول 2 ، والقيام بثماني خطوات أو أكثر في وقت سيجعلك تفوت النقطة حيث تصل إلى 1 للمرة الأولى. ثم تابع الحلقة "العادية" - أو قم بإعداد جدول يخبرك بعدد الخطوات الأخرى اللازمة للوصول إلى 1.

PS. أظن بشدة أن اقتراح بيتر كوردس سيجعله أسرع. لن يكون هناك أي فروع مشروطة على الإطلاق ما عدا واحد ، وسيتم التنبؤ بذلك بشكل صحيح إلا عندما تنتهي الحلقة فعليًا. لذلك سيكون رمز شيء من هذا القبيل

static const unsigned int multipliers [256] = { ... }
static const unsigned int adders [256] = { ... }

while (n > 128) {
    size_t lastBits = n % 256;
    n = (n >> 8) * multipliers [lastBits] + adders [lastBits];
}

في الممارسة العملية ، يمكنك قياس ما إذا كانت معالجة آخر 9 و 10 و 11 و 12 بت من n في وقت واحد ستكون أسرع. بالنسبة لكل بت ، سيتضاعف عدد الإدخالات في الجدول ، وأتوقع حدوث تباطؤ عندما لا تنسجم الجداول مع ذاكرة التخزين المؤقت L1 بعد الآن.

PPS. إذا كنت بحاجة إلى عدد من العمليات: في كل تكرار ننفذ بالضبط ثمانية أقسام على قسمين ، وعدد متغير من العمليات (3n + 1) ، لذلك فإن الطريقة الواضحة لحساب العمليات ستكون مجموعة أخرى. ولكن يمكننا بالفعل حساب عدد الخطوات (بناءً على عدد التكرارات للحلقة).

يمكننا إعادة تعريف المشكلة بشكل طفيف: استبدل n بـ (3n + 1)/2 إذا كانت غريبة ، واستبدل n بـ n/2 حتى لو كان. بعد ذلك ، ستنفذ كل عملية تكرار 8 خطوات بالضبط ، لكن يمكنك التفكير في أن الغش :-) لذلك افترض أنه كانت هناك عمليات r n <- 3n + 1 وعمليات s n <- n/2. ستكون النتيجة تمامًا n '= n * 3 ^ r/2 ^ s ، لأن n <- 3n + 1 تعني n <- 3n * (1 + 1/3n). بأخذ اللوغاريتم نجد r = (s + log2 (n '/ n))/log2 (3).

إذا قمنا بالحلقة حتى n ≤ 1،000،000 ولدينا جدول تم حسابه مسبقًا عدد التكرارات المطلوبة من أي نقطة بداية n ≤ 1،000،000 ، فإن حساب r كما هو موضح أعلاه ، مقربًا إلى أقرب عدد صحيح ، سيعطي النتيجة الصحيحة ما لم تكن s كبيرة حقًا.

21
gnasher729

في ملاحظة غير ذات صلة إلى حد ما: المزيد من الخارقة الأداء!

  • [أول «تخمين» تم كشفه أخيرًا بواسطة @ ShreevatsaR ؛ إزالة]

  • عند اجتياز التسلسل ، يمكننا فقط الحصول على 3 حالات ممكنة في الحي المجاور للعنصر الحالي N (معروض أولاً):

    1. [حتى] [غريب]
    2. [فردي زوجي]
    3. [حتى] [حتى]

    للقفز بعد هذين العنصرين يعني حساب (N >> 1) + N + 1 و ((N << 1) + N + 1) >> 1 و N >> 2 ، على التوالي.

    دعنا نثبت أنه في كلتا الحالتين (1) و (2) من الممكن استخدام الصيغة الأولى ، (N >> 1) + N + 1.

    الحالة (1) واضحة. تشير الحالة (2) إلى (N & 1) == 1 ، لذلك إذا افترضنا (بدون فقدان العمومية) أن N طولها 2 بت وأن وحداتها ba من الأكثر إلى الأقل أهمية ، ثم a = 1 ، والحالات التالية:

    (N << 1) + N + 1:     (N >> 1) + N + 1:
    
            b10                    b1
             b1                     b
           +  1                   + 1
           ----                   ---
           bBb0                   bBb
    

    حيث B = !b. التحول الصحيح النتيجة الأولى يعطينا بالضبط ما نريد.

    Q.E.D .: (N & 1) == 1 ⇒ (N >> 1) + N + 1 == ((N << 1) + N + 1) >> 1.

    كما ثبت ، يمكننا اجتياز عناصر التسلسل 2 في وقت واحد ، وذلك باستخدام عملية ثلاثية واحدة. آخر 2 × الحد من الوقت.

تبدو الخوارزمية الناتجة كما يلي:

uint64_t sequence(uint64_t size, uint64_t *path) {
    uint64_t n, i, c, maxi = 0, maxc = 0;

    for (n = i = (size - 1) | 1; i > 2; n = i -= 2) {
        c = 2;
        while ((n = ((n & 3)? (n >> 1) + n + 1 : (n >> 2))) > 2)
            c += 2;
        if (n == 2)
            c++;
        if (c > maxc) {
            maxi = i;
            maxc = c;
        }
    }
    *path = maxc;
    return maxi;
}

int main() {
    uint64_t maxi, maxc;

    maxi = sequence(1000000, &maxc);
    printf("%llu, %llu\n", maxi, maxc);
    return 0;
}

نحن هنا نقارن n > 2 لأن العملية قد تتوقف عند 2 بدلاً من 1 إذا كان الطول الإجمالي للتسلسل غريبًا.

[تصحيح:]

دعنا نترجم هذا إلى تجميع!

MOV RCX, 1000000;



DEC RCX;
AND RCX, -2;
XOR RAX, RAX;
MOV RBX, RAX;

@main:
  XOR RSI, RSI;
  LEA RDI, [RCX + 1];

  @loop:
    ADD RSI, 2;
    LEA RDX, [RDI + RDI*2 + 2];
    SHR RDX, 1;
    SHRD RDI, RDI, 2;    ror rdi,2   would do the same thing
    CMOVL RDI, RDX;      Note that SHRD leaves OF = undefined with count>1, and this doesn't work on all CPUs.
    CMOVS RDI, RDX;
    CMP RDI, 2;
  JA @loop;

  LEA RDX, [RSI + 1];
  CMOVE RSI, RDX;

  CMP RAX, RSI;
  CMOVB RAX, RSI;
  CMOVB RBX, RCX;

  SUB RCX, 2;
JA @main;



MOV RDI, RCX;
ADD RCX, 10;
Push RDI;
Push RCX;

@itoa:
  XOR RDX, RDX;
  DIV RCX;
  ADD RDX, '0';
  Push RDX;
  TEST RAX, RAX;
JNE @itoa;

  Push RCX;
  LEA RAX, [RBX + 1];
  TEST RBX, RBX;
  MOV RBX, RDI;
JNE @itoa;

POP RCX;
INC RDI;
MOV RDX, RDI;

@outp:
  MOV RSI, RSP;
  MOV RAX, RDI;
  SYSCALL;
  POP RAX;
  TEST RAX, RAX;
JNE @outp;

LEA RAX, [RDI + 59];
DEC RDI;
SYSCALL;

استخدم هذه الأوامر لتجميع:

nasm -f elf64 file.asm
ld -o file file.o

رؤية C ونسخة محسنة/من إصلاح bugfixed من قبل بيتر كوردس على Godbolt . (ملاحظة المحرر: نأسف لوضع إجابتي في إجابتك ، لكن إجابتي بلغت الحد المسموح به وهو 30 كيلو بايت من روابط Godbolt + text!)

18
hidefromkgb

تتم ترجمة برامج C++ إلى برامج التجميع أثناء إنشاء رمز الجهاز من التعليمات البرمجية المصدر. سيكون من الخطأ تقريبًا القول بأن التجميع أبطأ من C++. علاوة على ذلك ، يختلف الرمز الثنائي الذي تم إنشاؤه من برنامج التحويل البرمجي إلى برنامج التحويل البرمجي. لذلك فإن برنامج التحويل البرمجي C++ الذكي قد ينتج كودًا ثنائياً أكثر أمثل وفعالية من كود المجمّع البكم.

ومع ذلك أعتقد أن منهجية التنميط لديك بها عيوب معينة. فيما يلي إرشادات عامة للتوصيف:

  1. تأكد من أن النظام الخاص بك في حالته الطبيعية/الخمول. أوقف تشغيل جميع العمليات (التطبيقات) التي بدأت أو التي تستخدم وحدة المعالجة المركزية بشكل مكثف (أو استطلاع عبر الشبكة).
  2. يجب أن يكون حجم البيانات الخاص بك أكبر في الحجم.
  3. يجب تشغيل الاختبار لمدة تزيد عن 5 إلى 10 ثوانٍ.
  4. لا تعتمد على عينة واحدة فقط. أداء الاختبار N مرات. جمع النتائج وحساب متوسط ​​أو وسيط النتيجة.
5
Mangu Singh Rajpurohit

لمشكلة Collatz ، يمكنك الحصول على دفعة كبيرة في الأداء من خلال التخزين المؤقت "ذيول". هذا هو الوقت/الذاكرة المفاضلة. راجع: التحفيظ ( https://en.wikipedia.org/wiki/Memoization ). يمكنك أيضًا البحث في حلول البرمجة الديناميكية لمفاضلات الوقت/الذاكرة الأخرى.

مثال تنفيذ الثعبان:

import sys

inner_loop = 0

def collatz_sequence(N, cache):
    global inner_loop

    l = [ ]
    stop = False
    n = N

    tails = [ ]

    while not stop:
        inner_loop += 1
        tmp = n
        l.append(n)
        if n <= 1:
            stop = True  
        Elif n in cache:
            stop = True
        Elif n % 2:
            n = 3*n + 1
        else:
            n = n // 2
        tails.append((tmp, len(l)))

    for key, offset in tails:
        if not key in cache:
            cache[key] = l[offset:]

    return l

def gen_sequence(l, cache):
    for elem in l:
        yield elem
        if elem in cache:
            yield from gen_sequence(cache[elem], cache)
            raise StopIteration

if __== "__main__":
    le_cache = {}

    for n in range(1, 4711, 5):
        l = collatz_sequence(n, le_cache)
        print("{}: {}".format(n, len(list(gen_sequence(l, le_cache)))))

    print("inner_loop = {}".format(inner_loop))
5
Emanuel Landeholm

حتى بدون النظر إلى التجميع ، فإن السبب الأكثر وضوحًا هو أنه ربما تم تحسين /= 2 كـ >>=1 والعديد من المعالجات لديها عملية تحويل سريعة للغاية. ولكن حتى لو لم يكن لدى المعالج عملية تحويل ، فإن عدد صحيح هو أسرع من تقسيم النقطة العائمة.

تحرير: قد يختلف الأميال الخاص بك على عبارة "تقسيم عدد صحيح أسرع من تقسيم الفاصلة العائمة" أعلاه. تكشف التعليقات أدناه أن المعالجات الحديثة أعطت الأولوية لتحسين تقسيم fp على عدد صحيح. لذلك إذا كان شخص ما يبحث عن السبب الأكثر ترجيحًا للتسريع الذي يطرحه سؤال مؤشر الترابط هذا ، فإن تحسين المحول البرمجي /=2 كـ >>=1 سيكون أفضل مكان للبحث عنه.


في ملاحظة غير مرتبطة ، إذا كان n غريبًا ، فسيكون التعبير n*3+1 دائمًا متساويًا. لذلك ليست هناك حاجة للتحقق. يمكنك تغيير هذا الفرع ل

{
   n = (n*3+1) >> 1;
   count += 2;
}

وبالتالي فإن البيان كله يكون بعد ذلك

if (n & 1)
{
    n = (n*3 + 1) >> 1;
    count += 2;
}
else
{
    n >>= 1;
    ++count;
}
4
Dmitry Rubanovich

من التعليقات:

ولكن ، لا يتوقف هذا الرمز (بسبب تجاوز عدد صحيح)!؟! إيف داوست

بالنسبة إلى العديد من الأرقام ، سوف لا تجاوز السعة.

إذا كان will overflow - لأحد هذه البذور الأولية غير المحظوظة ، فمن المحتمل جدًا أن يتلاقى عدد الفائض في اتجاه واحد دون تجاوز آخر.

لا يزال هذا يطرح سؤالًا مثيرًا للاهتمام ، هل هناك عدد من البذور الزائدة الدائرية؟

أي سلسلة نهائية متقاربة بسيطة تبدأ بقوة ذات قيمة (واضحة بما فيه الكفاية؟).

2 ^ 64 سيتجاوز إلى صفر ، وهو حلقة لانهائية غير محددة وفقًا للخوارزمية (تنتهي فقط بـ 1) ، ولكن الحل الأمثل في الإجابة سينتهي بسبب shr rax إنتاج ZF = 1.

يمكن أن ننتج 2 ^ 64؟ إذا كان رقم البداية هو 0x5555555555555555 ، فهذا رقم فردي ، والرقم التالي هو 3n + 1 ، وهو 0xFFFFFFFFFFFFFFFF + 1 = 0. من الناحية النظرية في حالة غير محددة من الخوارزمية ، ولكن سيتم استرداد إجابة johnfound المحسنة من خلال الخروج على ZF = 1. cmp rax,1 لـ Peter Cordes ستنتهي بحلقة لانهائية (QED البديل 1 ، "cheapo" من خلال رقم 0 غير محدد).

ماذا عن بعض الأرقام الأكثر تعقيدًا ، والتي ستخلق دورة بدون 0؟ بصراحة ، لست متأكدًا ، نظرية نظري في الرياضيات ضبابية للغاية بحيث لا يمكنني الحصول على أي فكرة جادة ، وكيفية التعامل معها بطريقة جادة. لكن بشكل حدسي ، أود أن أقول إن السلسلة ستتقارب إلى 1 لكل رقم: 0 <number ، حيث إن الصيغة 3n + 1 ستحول ببطء كل ​​عامل أولي غير 2 للرقم الأصلي (أو الوسيط) إلى بعض القوة 2 ، عاجلاً أم آجلاً . لذلك لا داعي للقلق بشأن الحلقة اللانهائية للمسلسل الأصلي ، فالتجاوز فقط هو الذي يمكن أن يعيقنا.

لذلك أنا فقط وضعت أرقام قليلة في ورقة وألقيت نظرة على أرقام مبتورة 8 بت.

هناك ثلاث قيم تفيض إلى 0: 227 و 170 و 85 (85 انتقل مباشرة إلى 0 ، واثنان آخران يتجهان نحو 85).

ولكن ليس هناك قيمة لإنشاء البذور الفائضة الدورية.

من الممتع أن أكون قد قمت بالتحقق ، وهو أول رقم يعاني من اقتطاع 8 بت ، ويتأثر بالفعل 27! يصل إلى القيمة 9232 في السلسلة المناسبة غير المقطوعة (القيمة المقطوعة الأولى هي 322 في الخطوة الثانية عشرة) ، والحد الأقصى للقيمة التي تم التوصل إليها لأي من أرقام الإدخال 2-255 بطريقة غير مبتورة هي 13120 (لـ 255 نفسها) ، أقصى عدد من الخطوات للالتقاء إلى 1 حوالي 128 (+ -2 ، لست متأكدًا مما إذا كانت "1" ستحسب ، إلخ ...).

من المثير للاهتمام (بالنسبة لي) أن الرقم 9232 هو الحد الأقصى للعديد من أرقام المصدر الأخرى ، ما هو المميز في ذلك؟ : -O 9232 = 0x2410 ... hmmm .. لا فكرة.

لسوء الحظ ، لا يمكنني الحصول على أي فهم عميق لهذه السلسلة ، ولماذا تتلاقى وما هي آثار اقتطاعها إلى k بت ، لكن مع وضع cmp number,1 ، من الممكن بالتأكيد وضع الخوارزمية في حلقة لا نهائية مع قيمة إدخال معينة تنتهي كـ 0 بعد الاقتطاع.

لكن القيمة 27 التي تفيض لحالة 8 بت هي نوع من التنبيه ، يبدو أن هذا إذا كنت تحسب عدد الخطوات للوصول إلى القيمة 1 ، فسوف تحصل على نتيجة خاطئة لغالبية الأرقام من مجموعة الأعداد الصحيحة k-bit. بالنسبة لعدد صحيح من 8 بتات ، فإن 146 رقمًا من أصل 256 قد أثرت على السلسلة بالاقتطاع (ربما لا يزال بعضها يصل إلى العدد الصحيح من الخطوات عن طريق الصدفة ربما ، أنا كسول جدًا للتحقق من ذلك).

4
Ped7g

لم تنشر الكود الذي تم إنشاؤه بواسطة المترجم ، لذلك هناك بعض التخمينات هنا ، ولكن حتى بدون رؤيته ، يمكن للمرء أن يقول ما يلي:

test rax, 1
jpe even

... لديه فرصة بنسبة 50٪ لإساءة تقدير الفرع ، وسيكون ذلك مكلفًا.

من شبه المؤكد أن المحول البرمجي يقوم بإجراء كلا الحسابين (الذي يكلف بشكل أكثر تقلبًا نظرًا لأن div/mod عبارة عن زمن انتقال طويل للغاية ، وبالتالي فإن الضرب الإضافي "مجاني") ويتابع مع CMOV. التي ، بطبيعة الحال ، لديها صفر في المئة فرصة لسوء تقدير.

4
Damon

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

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

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

3
gnasher729