Как числа больше 2 ^ 32 обрабатываются 32-битным компьютером? - PullRequest
7 голосов
/ 10 октября 2010

Я пытаюсь понять, как на 32-битном компьютере выполняются вычисления с числами больше 2 32 .

C-код

$ cat size.c
#include<stdio.h>
#include<math.h>

int main() {

    printf ("max unsigned long long = %llu\n",
    (unsigned long long)(pow(2, 64) - 1));
}
$

gcc output

$ gcc size.c -o size
$ ./size
max unsigned long long = 18446744073709551615
$

Соответствующий ассемблерный код

$ gcc -S size.c -O3
$ cat size.s
    .file   "size.c"
    .section    .rodata.str1.4,"aMS",@progbits,1
    .align 4
.LC0:
    .string "max unsigned long long = %llu\n"
    .text
    .p2align 4,,15
.globl main
    .type   main, @function
main:
    pushl   %ebp
    movl    %esp, %ebp
    andl    $-16, %esp
    subl    $16, %esp
    movl    $-1, 8(%esp)   #1
    movl    $-1, 12(%esp)  #2
    movl    $.LC0, 4(%esp) #3
    movl    $1, (%esp)     #4
    call    __printf_chk
    leave
    ret
    .size   main, .-main
    .ident  "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
    .section    .note.GNU-stack,"",@progbits
$

Что именно происходит в строках 1 - 4?

Является ли это своего рода объединением строк вуровень сборки?

Ответы [ 7 ]

19 голосов
/ 10 октября 2010

__printf_chk - это обертка вокруг printf, которая проверяет переполнение стека и принимает дополнительный первый параметр, флаг (например, см. здесь .)

pow(2, 64) - 1 имеетбыл оптимизирован до 0xffffffffffffffff, поскольку аргументы являются константами.

Согласно обычным соглашениям о вызовах, первый аргумент __printf_chk() (int flag) - это 32-разрядное значение в стеке (в %esp во время call инструкции).Следующий аргумент const char * format - это 32-битный указатель (следующее 32-битное слово в стеке, то есть в %esp+4).И 64-разрядное количество, которое печатается, занимает следующие два 32-разрядных слова (в %esp+8 и %esp+12):

pushl   %ebp                 ; prologue
movl    %esp, %ebp           ; prologue
andl    $-16, %esp           ; align stack pointer
subl    $16, %esp            ; reserve bytes for stack frame
movl    $-1, 8(%esp)   #1    ; store low half of 64-bit argument (a constant) to stack
movl    $-1, 12(%esp)  #2    ; store high half of 64-bit argument (a constant) to stack
movl    $.LC0, 4(%esp) #3    ; store address of format string to stack
movl    $1, (%esp)     #4    ; store "flag" argument to __printf_chk to stack
call    __printf_chk         ; call routine
leave                        ; epilogue
ret                          ; epilogue

Компилятор эффективно переписал это:

printf("max unsigned long long = %llu\n", (unsigned long long)(pow(2, 64) - 1));

... в это:

__printf_chk(1, "max unsigned long long = %llu\n", 0xffffffffffffffffULL);

... и во время выполнения макет стека для вызова выглядит следующим образом (показывает стек в виде 32-битных слов с адресами, увеличивающимися свнизу диаграммы вверх):

        :                 :
        :     Stack       :
        :                 :
        +-----------------+
%esp+12 |      0xffffffff | \ 
        +-----------------+  } <-------------------------------------.
%esp+8  |      0xffffffff | /                                        |
        +-----------------+                                          |
%esp+4  |address of string| <---------------.                        |
        +-----------------+                 |                        |
%esp    |               1 | <--.            |                        |
        +-----------------+    |            |                        |
                  __printf_chk(1, "max unsigned long long = %llu\n", |
                                                    0xffffffffffffffffULL);
6 голосов
/ 10 октября 2010

аналогично тому, как мы обрабатываем числа больше 9, только с цифрами 0 - 9. (используя позиционные цифры)Предполагая, что вопрос является концептуальным.

3 голосов
/ 10 октября 2010

В вашем случае компилятор знает, что 2 ^ 64-1 это просто 0xffffffffffffffff, поэтому он поместил -1 (низкий dword) и -1 (высокий dword) в стек в качестве аргумента для printf.Это просто оптимизация.

Как правило, 64-разрядные числа (и даже большие значения) могут храниться с несколькими словами, например, unsigned long long использует два dword с.Чтобы добавить два 64-битных числа, выполняются два сложения - одно на младших 32 битах и ​​одно на старших 32 битах, плюс перенос:

; Add 64-bit number from esi onto edi:
mov     eax, [esi] ; get low 32 bits of source
add     [edi], eax ; add to low 32 bits of destination
; That add may have overflowed, and if it did, carry flag = 1.
mov     eax, [esi+4] ; get high 32 bits of source
adc     [edi+4], eax ; add to high 32 bits of destination, then add carry.

Вы можете повторить эту последовательность addи adc столько, сколько вы хотите добавить произвольно большие числа.То же самое можно сделать с вычитанием - просто используйте sub и sbb (вычитать с заимствованием).

Умножение и деление намного сложнее, и компилятор обычно создает несколько небольших вспомогательных функций для работы сэто всякий раз, когда вы умножаете 64-битные числа вместе.Пакеты типа GMP , которые поддерживают очень, очень большие целые числа, используют SSE / SSE2 для ускорения работы.Посмотрите эту статью Википедии для получения дополнительной информации об алгоритмах умножения.

2 голосов
/ 27 июля 2016

Как уже отмечали другие, вся 64-битная арифметика в вашем примере была оптимизирована. Этот ответ фокусируется на вопросе в заголовке.

По сути, мы рассматриваем каждое 32-разрядное число как цифру и работаем в базе 4294967296. Таким образом, мы можем работать с произвольно большими числами.

Сложение и вычитание проще всего. Мы работаем через цифры по одному, начиная с наименее значимого и переходя к наиболее значимому. Обычно первая цифра выполняется с помощью обычной инструкции сложения / вычитания, а последующие цифры - с использованием специальной инструкции «сложить с переносом» или «вычесть с заимствованием». Флаг переноса в регистре состояния используется для переноса бита переноса / заимствования из одной цифры в другую. Благодаря двойному дополнению подписи и без знака сложения и вычитания одинаковы.

Умножение немного сложнее, умножение двух 32-разрядных цифр может дать 64-разрядный результат. Большинство 32-битных процессоров будут иметь инструкции, которые умножают два 32-битных числа и выдают 64-битный результат в двух регистрах. Дополнение будет необходимо, чтобы объединить результаты в окончательный ответ. Благодаря двойному дополнению подпись и умножение без знака одинаковы при условии, что желаемый размер результата совпадает с размером аргумента. Если результат больше аргументов, требуется особая осторожность.

Для сравнения начнем с самой значимой цифры. Если оно равно, мы переходим к следующей цифре, пока результаты не будут равны.

Деление слишком сложно для меня, чтобы описать в этом посте, но есть множество примеров алгоритмов. например http://www.hackersdelight.org/hdcodetxt/divDouble.c.txt


Некоторые примеры из gcc https://godbolt.org/g/NclqXC, ассемблер в синтаксисе intel.

Первое дополнение. добавление двух 64-битных чисел и получение 64-битного результата. Asm одинакова для подписанной и неподписанной версий.

int64_t add64(int64_t a, int64_t b) { return a +  b; }
add64:
    mov     eax, DWORD PTR [esp+12]
    mov     edx, DWORD PTR [esp+16]
    add     eax, DWORD PTR [esp+4]
    adc     edx, DWORD PTR [esp+8]
    ret

Это довольно просто: загрузить один аргумент в eax и edx, затем добавить другой, используя add, а затем add с переносом. Результат сохраняется в eax и edx для возврата вызывающей стороне.

Теперь умножение двух 64-битных чисел для получения 64-битного результата. Снова код не меняется с подписанного на неподписанный. Я добавил несколько комментариев, чтобы было легче следить.

Прежде чем мы рассмотрим код, давайте рассмотрим математику. a и b - 64-разрядные числа. Я буду использовать lo () для представления младших 32-разрядных чисел 64-разрядного числа и hi () для представления старших 32-разрядных чисел 64-разрядного числа.

(a * b) = (lo (a) * lo (b)) + (hi (a) * lo (b) * 2 ^ 32) + (hi (b) * lo (a) * 2 ^ 32) + (привет (б) * привет (а) * 2 ^ 64)

(a * b) mod 2 ^ 64 = (lo (a) * lo (b)) + (lo (hi (a) * lo (b)) * 2 ^ 32) + (lo (hi (b) ) * lo (a)) * 2 ^ 32)

lo ((a * b) mod 2 ^ 64) = lo (lo (a) * lo (b))

hi ((a * b) mod 2 ^ 64) = hi (lo (a) * lo (b)) + lo (hi (a) * lo (b)) + lo (hi (b) * lo (а))

uint64_t mul64(uint64_t a, uint64_t b) { return a*b; }
mul64:
    push    ebx                      ;save ebx
    mov     eax, DWORD PTR [esp+8]   ;load lo(a) into eax
    mov     ebx, DWORD PTR [esp+16]  ;load lo(b) into ebx
    mov     ecx, DWORD PTR [esp+12]  ;load hi(a) into ecx  
    mov     edx, DWORD PTR [esp+20]  ;load hi(b) into edx
    imul    ecx, ebx                 ;ecx = lo(hi(a) * lo(b))
    imul    edx, eax                 ;edx = lo(hi(b) * lo(a))
    add     ecx, edx                 ;ecx = lo(hi(a) * lo(b)) + lo(hi(b) * lo(a))
    mul     ebx                      ;eax = lo(low(a) * lo(b))
                                     ;edx = hi(low(a) * lo(b))
    pop     ebx                      ;restore ebx.
    add     edx, ecx                 ;edx = hi(low(a) * lo(b)) + lo(hi(a) * lo(b)) + lo(hi(b) * lo(a))
    ret

Наконец, когда мы пробуем деление, мы видим.

int64_t div64(int64_t a, int64_t b) { return a/b; }
div64:
    sub     esp, 12
    push    DWORD PTR [esp+28]
    push    DWORD PTR [esp+28]
    push    DWORD PTR [esp+28]
    push    DWORD PTR [esp+28]
    call    __divdi3
    add     esp, 28
    ret

Компилятор решил, что деление слишком сложно для реализации inline, и вместо этого вызывает библиотечную процедуру.

1 голос
/ 10 октября 2010

Как упоминает @Pafy, компилятор оценил это как константу.

2 до 64-го минус 1 равно 0xffffffffffffffff.

Как 2 32-битных целых числа это: 0xffffffff и 0xffffffff,который, если вы берете это как пару 32-битных типов со знаком, заканчивается как: -1 и -1.

Таким образом, для вашего компилятора сгенерированный код оказывается эквивалентным:

printf("max unsigned long long = %llu\n", -1, -1);

В сборке написано так:

movl    $-1, 8(%esp)   #Second -1 parameter
movl    $-1, 12(%esp)  #First -1 parameter
movl    $.LC0, 4(%esp) #Format string
movl    $1, (%esp)     #A one.  Kind of odd, perhaps __printf_chk
                       #in your C library expects this.
call    __printf_chk

Кстати, лучший способ вычислить степени 2 - сдвинуть 1 влево.Например.(1ULL << 64) - 1.

1 голос
/ 10 октября 2010

Компилятор фактически сделал статическую оптимизацию вашего кода. строки # 1 # 2 # 3 являются параметрами для printf ()

0 голосов
/ 19 октября 2014

Никто в этой теме не заметил, что ОП попросил объяснить первые 4 строки, а не строки 11-14.

Первые 4 строки:

    .file   "size.c"
    .section    .rodata.str1.4,"aMS",@progbits,1
    .align 4
.LC0:

Вот что происходит в первых 4 строках:

.file   "size.c"

Это директива на ассемблере, в которой говорится, что мы собираемся запустить новый логический файл с именем "size.c".

.section    .rodata.str1.4,"aMS",@progbits,1

Это также директива для строк только для чтения в программе.

.align 4

Эта директива устанавливает счетчик местоположения всегда кратным 4.

.LC0:

Это метка LC0, к которой можно перейти, например.

Я надеюсь, что дал правильный ответ на вопрос, так как я точно ответил на вопрос, заданный ОП.

...