Сумма всех квадратов от 1 до 100 в сборке

#assembly #x86

#сборка #x86

Вопрос:

  mov eax,0
 mov ecx,100 

loop2_start: mov eax,ecx
             add eax,ecx 
             imul eax,eax  ;To act as n^2
             jle loop2_start

loop2_end: call print_int
           call print_nl
  

Я пытаюсь найти сумму всех квадратов от 1 до 100. Когда я печатаю это, это дает мне 40000, когда ответ должен быть 338350. Что я должен изменить, чтобы заставить его работать? Возможно, моя инструкция перехода?

Комментарии:

1. Добавление некоторых комментариев о том, что, по вашему мнению, делает каждая инструкция, было бы очень полезно для тех, кто пытается ответить на этот вопрос. Или даже просто высокоуровневая идея подхода. Я ожидал inc dec где-то или, но я его не вижу, поэтому я понятия не имею об используемом вами алгоритме

2. Просто подсказка: 200 * 200 равно 40 000. Это то, что делает ваш код.

3. не вижу cmp

4. Как бы я использовал cmp ?

5. Напишите логику на C, или псевдокод, или что-то еще, включая логику цикла, и переведите это в asm. (Имея в виду asm, вы можете использовать do{}while() структуру цикла). Ваш текущий код между loop2_start и jle полностью сломан, выбросьте его и начните сначала.

Ответ №1:

Давайте начнем с некоторой математики.

Для положительных значений n , если:

 lastSquare = n*n
  

Затем:

 nextSquare = (n 1)*(n 1)
           = (n * n)   (n * 1)   (1 * n)   1
           = (n * n)   2 * n   1
           = lastSquare   2 * n   1
  

Другими словами, (используя псевдокод, подобный C), вы можете сделать:

     square = 0;
    sum = 0;
    for(n = 1-1; n < 100-1; n  ) {
        square = square   2 * n   1;
        sum = sum   square;
    }
  

Для сборки 80×86 square = square 2 * n 1 это может быть выполнено с помощью одной быстрой инструкции — например lea ebx,[ebx ecx*2 1] .

Это дает что-то вроде (синтаксис NASM, непроверенный):

 ; Loop preperation

    xor ebx,ebx           ;ebx = square = 0
    xor eax,eax           ;eax = sum = 0
    xor ecx,ecx           ;ecx = n = 1-1

; Loop

.next:
    lea ebx,[ebx ecx*2 1] ;ebx = square   2 * n   1
    add eax,ebx           ;eax = sum   square

; Loop end

    inc ecx
    cmp ecx,100-1
    jb .next
  

Однако; середина цикла мала по сравнению с накладными расходами цикла, поэтому было бы интересно немного развернуть цикл, чтобы уменьшить накладные расходы цикла. 99 нечетно, но кратно 3, поэтому:

 ; Loop preperation

    xor ebx,ebx           ;ebx = square = 0
    xor eax,eax           ;eax = sum = 0
    xor ecx,ecx           ;ecx = n = 1-1

; Loop

.next:
    lea ebx,[ebx ecx*2 1]   ;ebx = square   2 * n   1
    add eax,ebx             ;eax = sum   square
    lea ebx,[ebx ecx*2 2 1] ;ebx = square   2 * (n 1)   1
    add eax,ebx             ;eax = sum   square
    lea ebx,[ebx ecx*2 4 1] ;ebx = square   2 * (n 2)   1
    add eax,ebx             ;eax = sum   square

; Loop end

    add ecx,3
    cmp ecx,100-1
    jb .next
  

Это позволяет вам использовать математику для большего мошенничества! В частности:

 nextSquare = (n 1)*(n 1)
           = (n * n)   (n * 1)   (1 * n)   1
           = (n * n)   2 * n   1
           = lastSquare   2 * n   1

nextNextSquare = (n 2)*(n 2)
               = (n * n)   (n * 2)   (2 * n)   2*2
               = (n * n)   4 * n   4
               = lastSquare   4 * n   4

nextNextNextSquare = (n 3)*(n 3)
                   = (n * n)   (n * 3)   (3 * n)   3*3
                   = (n * n)   6 * n   9
                   = lastSquare   6 * n   9

nextAddend = nextSquare    nextNextSquare   nextNextNextSquare
           = lastSquare   2 * n   1   lastSquare   4 * n   4   lastSquare   6 * n   9
           = 3 * lastSquare   12 * n   14
           = 3 * (lastSquare   4 * n)   14
  

Это приводит к чему-то вроде (синтаксис NASM, непроверенный):

 ; Loop preperation

    xor ebx,ebx           ;ebx = square = 0
    xor eax,eax           ;eax = sum = 0
    xor ecx,ecx           ;ecx = n = 1-1

; Loop

.next:
    lea edx,[ebx ecx*4]      ;edx = square   4 * n
    lea edx,[edx*2 edx 14]   ;edx = 3 * (square   4 * n)   14
    add eax,edx              ;eax = sum   nextSquare    nextNextSquare   nextNextNextSquare
    lea edx,[ecx*2 ecx]      ;edx = 2*n
    lea ebx,[ebx 2*edx 9]    ;ebx = lastSquare   6 * n   9 = nextNextNextSquare


; Loop end

    add ecx,3
    cmp ecx,100-1
    jb .next
  

Конечно, разворачивая цикл больше, вы больше обманываете; но если вы продолжаете это делать и оптимизируете, вы в конечном итоге «полностью развернуты» (больше нет цикла), где код становится одной mov eax,precalculated_constant инструкцией «».

Комментарии:

1. lea ebx,[ebx ecx*2 1] эффективен для общего количества операций ввода-вывода, но имеет задержку в 3 цикла (на процессорах Intel, потому что имеет 3 «компонента», 2 плюса), поэтому в этом цикле на самом деле было бы быстрее выполнять mov / imul / add, имея только цепочку зависимостей с циклом в 1 цикл. Или lea edx, [NOSPLIT ecx*2 1] / add ebx, edx было бы очень хорошим компромиссом.

2. Или мы могли бы сделать lea ebx,[ebx ecx*2] внутри цикла и add ebx, 100 после цикла, но тогда мы теряем итоговый результат. При развертывании вы можете свернуть 1 3 5 в один из 3 LEA, чтобы амортизировать эту дополнительную задержку… (lea с [ebx ecx*2] имеет дополнительную задержку на AMD из-за масштабируемого индекса. И делая это таким образом, это является частью цепочки зависимостей, передаваемой циклом. :/)

3. Если вы действительно хотите обмануть, byjus.com/maths/sum-of-squares показывает , что существует закрытая форма Σn^2 = [n(n 1)(2n 1)]/6 . Для большинства n случаев, когда конечный результат не переполняется, промежуточный результат (до деления на 6) также не переполняется.

4. @PeterCordes: если вы делаете взвешенную сумму для всех 32-разрядных процессоров 80×86 (от 80386 до настоящего времени, включая AMD, VIA, Vortex, Cryix и т. Д.), 3-компонентный LEA явно лучше, чем только MUL (без добавления) для любого разумного набора весов. Если вы игнорируете все, кроме последних процессоров для настольных компьютеров / серверов от Intel (исключая Atom, Knight’s Landing и т. Д.), MUL все равно никогда не будет лучше (например, задержка в 3 цикла для Coffee Lake), и вы все равно предпочтете 3-компонентный LEA (используя «предполагаемую стоимость нано-джоуля» в качестве тай-брейка). Для разделения (например, LEA затем ADD) набор весов имеет гораздо большее значение (например, bad на Zen2).

5.Взвешивание по текущей установленной базе? Я не совсем уверен; существует множество процессоров семейства Sandybridge, а большая часть остальных — Core2 / Nehalem / Zen: все они имеют пропускную способность в 1 цикл imul. Ключевым моментом является то, что imul задержка находится вне критического пути; это независимая работа, которая «разветвляется» на 1 цикл inc или dec цепочку, поэтому важна пропускная способность. Вы получаете только длинные зависимости, переносимые циклом, с этим уменьшением прочности, которое вычисляет следующий на основе предыдущего, а не просто полностью умножает. mov edx, ecx / imul edx,edx / add eax, edx только добавление — это критический путь с широтой