Что происходит на уровне сборки, когда у вас есть функции с большими входами - PullRequest
0 голосов
/ 02 мая 2018

Хотя JavaScript напрямую не становится сборкой, он должен продемонстрировать общий вопрос: как будет выглядеть высокоуровневая функция, если бы она была реализована в сборке , если входные данные функции большие . Скажем, например, этот случай:

myfunc(1, 2, 3)

Переменные существуют в виде небольших целых чисел, поэтому они могут быть помещены в отдельные регистры. Но скажем, у вас есть:

var a = 'some markdown readme...'
myfunc('my really long string', a, 'etc.')

Хотите знать, как это будет сделано в сборке (на высоком уровне).

Не похоже, что стек вызовов сборки будет использоваться для хранения этих значений, потому что они большие. Может быть, он хранит адрес памяти и его смещение (но если он динамический ...). Интересно узнать, как это работает.

Ответы [ 2 ]

0 голосов
/ 03 мая 2018

Массивы (включая строки) передаются по ссылке на большинстве языков высокого уровня. int foo(char*) просто получает значение указателя в виде аргумента, а указатель обычно представляет собой одно машинное слово (т.е. вписывается в регистр). В хороших современных соглашениях о вызовах первые несколько целочисленных / указательных аргументов обычно передаются в регистрах.

В C / C ++ нельзя передавать пустой массив по значению. Учитывая int arr[16]; func(arr);, функция func получает только указатель (на первый элемент).

В некоторых других языках более высокого уровня массивы могут больше походить на C ++ std::vector, поэтому вызываемый может иметь возможность увеличивать / уменьшать массив и определять его длину без отдельного аргумента. Это обычно означает, что есть «блок управления».

В C и C ++ вы можете передавать структуры по значению, а затем в правилах соглашения о вызовах указывается, как их передавать .

x86-64 Система V, например, передает структуры по 16 байтов или менее, упакованные в до 2 целочисленных регистров. Более крупные структуры копируются в стек независимо от размера элемента массива, который они содержат ( Какой тип данных C11 представляет собой массив в соответствии с AMD64 ABI ). (Так что не передавайте гигантские объекты по значению не встроенным функциям!)

Соглашение о вызовах Windows x64 передает большие структуры по скрытой ссылке.

* * Пример тысячи двадцать-одина * +1022 *: * +1023 *

typedef struct {
    // too big makes the asm output cluttered with loops or memcpy
    // int Big_McLargeHuge[1024*1024];
    int arr[4];
    long long a,b; //,c,d;
} bigobj;
// total 32 bytes with int=4, long long=8 bytes

int func(bigobj a);
int foo(bigobj a) {
    a.arr[3]++;
    return func(a);
}

выход source + asm в проводнике компилятора Godbolt .

Вы можете попробовать другие архитектуры на Godbolt с их стандартными соглашениями о вызовах, такие как ARM или AArch64. Я выбрал x86-64, потому что случайно узнал об интересной разнице в двух основных соглашениях о вызовах на одной платформе для передачи структуры.

x86-64 System V (gcc7.3 -O3) : foo имеет действительную копию значения arg (сделанную его вызывающей стороной), которую он может изменить, так что он делает поэтому и использует его в качестве аргумента для хвостового вызова. (Если он не может выполнить хвостовой вызов, ему придется сделать еще одну полную копию. Этот пример искусственно заставляет System V выглядеть действительно хорошо).

foo(bigobj):
    add     DWORD PTR [rsp+20], 1   # increment the struct member in the arg on the stack
    jmp     func(bigobj)            # tailcall func(a)

x86-64 Windows (MSVC CL19 /Ox) : обратите внимание, что мы обращаемся к a.arr [3] через RCX, первое целое число / указатель arg. Так что есть скрытая ссылка, но это не const-ссылка. Эта функция была вызвана по значению, но она изменяет данные, полученные по ссылке. Поэтому вызывающий должен сделать копию или, по крайней мере, предположить, что вызываемый объект уничтожил аргумент, на который он получил указатель. (Копия не требуется, если объект после этого мертв, но это возможно только для локальных объектов структуры, но не для передачи указателя на глобальный объект или что-то в этом роде).

$T1 = 32    ; offset of the tmp copy in this function's stack frame
foo PROC
    sub      rsp, 72              ; 00000048H     ; 32B of shadow space + 32B bigobj + 8 to align
    inc      DWORD PTR [rcx+12]
    movups   xmm0, XMMWORD PTR [rcx]              ; load modified `a`
    movups   xmm1, XMMWORD PTR [rcx+16]           ; apparently alignment wasn't required
    lea      rcx, QWORD PTR $T1[rsp]
    movaps   XMMWORD PTR $T1[rsp], xmm0
    movaps   XMMWORD PTR $T1[rsp+16], xmm1         ; store a copy
    call     int __cdecl func(struct bigobj)
    add      rsp, 72              ; 00000048H
    ret      0
foo ENDP

Создание другой копии объекта представляется пропущенной оптимизацией. Я думаю, что это будет правильной реализацией foo для того же соглашения о вызовах:

foo:
    add      DWORD PTR [rcx+12], 1       ; more efficient than INC because of the memory dst, on Intel CPUs
    jmp      func                        ; tailcall with pointer still in RCX

x86-64 clang для SysV ABI также пропускает оптимизацию, найденную gcc7.3, и копирует как MSVC .

Так что разница в ABI менее интересна, чем я думал; в обоих случаях вызываемый объект «владеет» аргументом, хотя для Windows он не гарантированно находится в стеке. Я предполагаю, что это позволяет динамическое распределение для передачи очень больших объектов по значению без переполнения стека, но это отчасти бессмысленно. Только не делай этого в первую очередь.


Мелкие предметы:

x86-64 Система V передает небольшие объекты, упакованные в регистры. Clang находит аккуратную оптимизацию, если вы закомментируете членов long long, поэтому у вас просто есть

typedef struct {
    int arr[4];
    //    long long a,b; //,c,d;
} bigobj;

# clang6.0 -O3
foo(bigobj):                          # @foo(bigobj)
    movabs  rax, 4294967296    # 0x100000000 = 1ULL << 32
    add     rsi, rax
    jmp     func(bigobj)          # TAILCALL

(arr[0..1] упакован в RDI, а arr[2..3] упакован в RSI, первые 2 регистра передачи аргументов целого числа / указателя в x86-64 SysV ABI).

gcc сам распаковывает arr[3] в регистр, где он может увеличивать его.

Но clang вместо распаковки и перепаковки увеличивает 32 старших бита RSI, добавляя 1ULL<<32.

MSVC все еще проходит по скрытой ссылке и копирует весь объект.

0 голосов
/ 02 мая 2018

Почему бы просто не попробовать?

   const char str[]="some string, doesnt matter how long";
    void more_fun ( const char *, const char *, int);

    void fun ( void )
    {
        more_fun(str,"hello world",5);
    }

фиктивная функция для радости компоновщика

.globl more_fun
more_fun:
    bx lr

архитектура на самом деле не имеет отношения к этому вопросу, компиляторы решают эту конкретную проблему таким же образом с наборами команд общего назначения, которые имеют базовый набор режимов адресации и т. Д., Поэтому, если есть исключение из этого, я не говорю о этих платформах, но x86, arm, mips, powerpc и т. д. и т. д. попадают в эту категорию.

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

Disassembly of section .text:

00001000 <fun>:
    1000:   e92d4010    push    {r4, lr}
    1004:   e3a02005    mov r2, #5
    1008:   e59f100c    ldr r1, [pc, #12]   ; 101c <fun+0x1c>
    100c:   e59f000c    ldr r0, [pc, #12]   ; 1020 <fun+0x20>
    1010:   eb000003    bl  1024 <more_fun>
    1014:   e8bd4010    pop {r4, lr}
    1018:   e12fff1e    bx  lr
    101c:   0000104c    andeq   r1, r0, r12, asr #32
    1020:   00001028    andeq   r1, r0, r8, lsr #32

00001024 <more_fun>:
    1024:   e12fff1e    bx  lr

Disassembly of section .rodata:

00001028 <str>:
    1028:   656d6f73    strbvs  r6, [sp, #-3955]!   ; 0xfffff08d
    102c:   72747320    rsbsvc  r7, r4, #32, 6  ; 0x80000000
    1030:   2c676e69    stclcs  14, cr6, [r7], #-420    ; 0xfffffe5c
    1034:   656f6420    strbvs  r6, [pc, #-1056]!   ; c1c <fun-0x3e4>
    1038:   20746e73    rsbscs  r6, r4, r3, ror lr
    103c:   7474616d    ldrbtvc r6, [r4], #-365 ; 0xfffffe93
    1040:   68207265    stmdavs r0!, {r0, r2, r5, r6, r9, r12, sp, lr}
    1044:   6c20776f    stcvs   7, cr7, [r0], #-444 ; 0xfffffe44
    1048:   00676e6f    rsbeq   r6, r7, pc, ror #28
    104c:   6c6c6568    cfstr64vs   mvdx6, [r12], #-416 ; 0xfffffe60
    1050:   6f77206f    svcvs   0x0077206f
    1054:   00646c72    rsbeq   r6, r4, r2, ror r12

, поскольку он был от objdump, он просто пытался разобрать строку, как если бы это были инструкции, поэтому играйте в разборке для частей текста.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...