Dostęp do wartości tablicy poprzez arytmetykę wskaźnika a zapisywanie w C

Czytam, że w C używanie arytmetyki wskaźników jest na ogół szybsze niż zapisywanie dostępu do tablicy. Czy to prawda nawet przy nowoczesnych (rzekomo-optymalizujących) kompilatorach?

Jeśli tak, to czy nadal tak jest, ponieważ zaczynam odchodzić od nauki C do Objective-C i Cocoa na Macach?

Jaki jest preferowany styl kodowania dla dostępu do tablicy, zarówno w C, jak i Objective-C? Który jest rozpatrywany (przez profesjonalistów ich języków) więcej czytelne, bardziej " poprawne "(z braku lepszego określenia)?

Author: Peter Mortensen, 2008-10-24

11 answers

Musisz zrozumieć przyczynę tego twierdzenia. Zastanawiałeś się kiedyś, dlaczego jest szybciej? Porównajmy jakiś kod:

int i;
int a[20];

// Init all values to zero
memset(a, 0, sizeof(a));
for (i = 0; i < 20; i++) {
    printf("Value of %d is %d\n", i, a[i]);
}

Wszystkie są zerowe, co za niespodzianka :-P pytanie brzmi, co oznacza a[i] właściwie w niskopoziomowym kodzie maszynowym? Oznacza

  1. Zapamiętaj adres a.

  2. Dodaj i do tego adresu wielkość pojedynczej pozycji a (int zwykle wynosi cztery bajty).

  3. Pobierz wartość z tego adresu.

Więc za każdym razem, gdy pobierasz wartość z a, adres bazowy a jest dodawany do wyniku mnożenia i przez cztery. Jeśli po prostu dereference wskaźnik, Krok 1. i 2. nie trzeba wykonywać, tylko krok 3.

Rozważ poniższy kod.

int i;
int a[20];
int * b;

memset(a, 0, sizeof(a));
b = a;
for (i = 0; i < 20; i++) {
    printf("Value of %d is %d\n", i, *b);
    b++;
}

Ten kodmoże być szybszy... ale nawet jeśli tak, różnica jest niewielka. Dlaczego może być szybciej? "*b " jest taki sam jak krok 3. z góry. Jednak "b++" nie jest to samo, co krok 1. i etap 2. "b++" zwiększy wskaźnik o 4.

(Ważne dla początkujących : running ++ na wskaźniku nie zwiększy wskaźnik jeden bajt w pamięci! Będzie zwiększ wskaźnik o tyle bajtów w pamięci jako dane, na które wskazuje jest w rozmiarze. Wskazuje na int i int to cztery bajty na mojej maszynie, więc b++ zwiększa b o cztery!)

Ok, ale dlaczego może być szybciej? Ponieważ dodanie czterech do wskaźnika jest szybsze niż mnożenie i przez cztery i dodawanie tego do wskaźnika. W obu przypadkach masz dodatek, ale w drugim nie masz mnożenia (unikasz czasu procesora potrzebnego do jednego mnożenia). Biorąc pod uwagę szybkość nowoczesnych procesorów, nawet jeśli tablica była 1 Mio elementów, zastanawiam się, czy naprawdę można porównać różnicę, choć.

To, że nowoczesny kompilator może zoptymalizować oba Kompilatory tak, aby były równie szybkie, jest czymś, co można sprawdzić, patrząc na wyjście złożenia, które produkuje. Ty tak poprzez przekazanie opcji" - S " (capital S) do GCC.

Oto kod pierwszego kodu C (został użyty poziom optymalizacji -Os, co oznacza optymalizację pod kątem rozmiaru i szybkości kodu, ale nie rób optymalizacji prędkości, która znacznie zwiększy rozmiar kodu, w przeciwieństwie do -O2 i znacznie różni się od -O3):

_main:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %edi
    pushl   %esi
    pushl   %ebx
    subl    $108, %esp
    call    ___i686.get_pc_thunk.bx
"L00000000001$pb":
    leal    -104(%ebp), %eax
    movl    $80, 8(%esp)
    movl    $0, 4(%esp)
    movl    %eax, (%esp)
    call    L_memset$stub
    xorl    %esi, %esi
    leal    LC0-"L00000000001$pb"(%ebx), %edi
L2:
    movl    -104(%ebp,%esi,4), %eax
    movl    %eax, 8(%esp)
    movl    %esi, 4(%esp)
    movl    %edi, (%esp)
    call    L_printf$stub
    addl    $1, %esi
    cmpl    $20, %esi
    jne L2
    addl    $108, %esp
    popl    %ebx
    popl    %esi
    popl    %edi
    popl    %ebp
    ret

To samo z drugim kodem:

_main:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %edi
    pushl   %esi
    pushl   %ebx
    subl    $124, %esp
    call    ___i686.get_pc_thunk.bx
"L00000000001$pb":
    leal    -104(%ebp), %eax
    movl    %eax, -108(%ebp)
    movl    $80, 8(%esp)
    movl    $0, 4(%esp)
    movl    %eax, (%esp)
    call    L_memset$stub
    xorl    %esi, %esi
    leal    LC0-"L00000000001$pb"(%ebx), %edi
L2:
    movl    -108(%ebp), %edx
    movl    (%edx,%esi,4), %eax
    movl    %eax, 8(%esp)
    movl    %esi, 4(%esp)
    movl    %edi, (%esp)
    call    L_printf$stub
    addl    $1, %esi
    cmpl    $20, %esi
    jne L2
    addl    $124, %esp
    popl    %ebx
    popl    %esi
    popl    %edi
    popl    %ebp
    ret
Na pewno jest inaczej. Różnica liczb 104 i 108 pochodzi ze zmiennej b (w pierwszym kodzie była jedna zmienna mniej na stosie, teraz mamy jeszcze jeden, zmieniając adresy stosów). Prawdziwa różnica kodu w pętli {[21] } wynosi
movl    -104(%ebp,%esi,4), %eax

W porównaniu do

movl    -108(%ebp), %edx
movl    (%edx,%esi,4), %eax

Dla mnie to raczej wygląda na to, że pierwsze podejście jest szybsze (!), ponieważ wydaje jeden kod maszynowy CPU, aby wykonać całą pracę (CPU robi to wszystko za nas), zamiast mieć dwa kody maszynowe. Z drugiej strony, dwa poniższe polecenia złożenia mogą mieć w sumie niższy czas pracy niż powyższe.

Jako słowo końcowe, Powiedziałbym, że w zależności od kompilatora i możliwości procesora (Jakie polecenia oferują Procesory, aby uzyskać dostęp do pamięci w jaki sposób), wynik może być w obu kierunkach. Każdy z nich może być szybszy / wolniejszy. Nie możesz powiedzieć na pewno, chyba że ograniczysz się dokładnie do jednego kompilatora (co oznacza również jedną wersję) i jednego konkretnego procesora. Ponieważ procesory mogą zrobić więcej i więcej w jednym poleceniu assembly (wieki temu kompilator naprawdę musiał ręcznie pobrać adres, pomnożyć i przez cztery i dodać oba razem przed pobraniem wartości), stwierdzenia, które kiedyś były prawdą absolutną wieki temu są obecnie coraz bardziej wątpliwe. Również kto wie, jak procesory działają wewnętrznie? Powyżej porównuję jedną instrukcję montażu do dwóch innych.

Widzę, że liczba instrukcji jest różna i czas potrzebny na taką instrukcję również może być różny. Również ilość pamięci, jakiej te instrukcje potrzebują w swojej prezentacji maszyny (W końcu muszą być przeniesione z pamięci do pamięci podręcznej procesora), jest inna. Jednak nowoczesne procesory nie wykonują instrukcji w sposób, w jaki je karmisz. Dzielą one duże instrukcje (często określane jako CISC) na małe instrukcje podrzędne (często określane jako RISC), co pozwala im również lepiej zoptymalizować przepływ programu pod kątem prędkości wewnętrznej. W rzeczywistości pierwsza, pojedyncza instrukcja i dwie pozostałe instrukcje poniżej mogą skutkować tym samym zbiorem instrukcji podrzędnych , w którym to przypadku nie ma mierzalnej różnicy prędkości.

Odnośnie Objective-C, to tylko C z rozszerzeniami. Tak więc wszystko, co jest prawdziwe dla C, będzie prawdziwe dla Objective-C, jak również pod względem wskaźników i tablic. Jeśli używasz obiektów z drugiej strony (na przykład NSArray lub NSMutableArray), jest to zupełnie inna bestia. Jednak w takim przypadku musisz I tak uzyskać dostęp do tych tablic metodami, nie ma dostępu do wskaźnika / tablicy do wyboru.

 77
Author: Mecki,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-10-01 13:48:00

" używanie arytmetyki wskaźników jest ogólnie szybsze niż zapisywanie tablic access "

Nie. Tak czy siak, to ta sama operacja. Subscripting jest cukrem składniowym służącym do dodawania (Rozmiar elementu * index ) do adresu początkowego tablicy.

To powiedziawszy, gdy iteracja elementów w tablicy, pobieranie wskaźnika do pierwszego elementu i zwiększanie go za każdym razem przez pętlę będzie zwykle nieco szybsze niż obliczanie pozycji bieżącego elementu z tablicy zmienna pętli za każdym razem. (Choć jest to niezwykłe, aby miało to znaczenie w prawdziwym życiu. Najpierw zbadaj swój algorytm, przedwczesna optymalizacja jest źródłem wszelkiego zła itp.)

 10
Author: moonshadow,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-10-24 11:38:49

To może być nieco off topic (sorry), ponieważ nie odpowiada na twoje pytanie dotyczące szybkości wykonania, ale powinieneś wziąć pod uwagę, że przedwczesna optymalizacja jest źródłem wszelkiego zła (Knuth). Moim zdaniem, szczególnie kiedy jeszcze (ponownie)uczymy się języka, napisz go tak, jak najłatwiej jest go najpierw przeczytać. Następnie, jeśli twój program działa poprawnie, rozważ optymalizację prędkości. Większość czasu kod będzie wystarczająco szybki i tak.

 5
Author: TheMarko,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-10-24 12:02:20

Mecki ma świetne wytłumaczenie. Z mojego doświadczenia wynika, że jedną z rzeczy, które często mają znaczenie przy indeksowaniu kontra wskaźnikach, jest to, co inny kod znajduje się w pętli. Przykład:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <iostream>

using namespace std;

typedef int64_t int64;
static int64 nsTime() {
  struct timespec tp;
  clock_gettime(CLOCK_REALTIME, &tp);
  return tp.tv_sec*(int64)1000000000 + tp.tv_nsec;
}

typedef int T;
size_t const N = 1024*1024*128;
T data[N];

int main(int, char**) {
  cout << "starting\n";

  {
    int64 const a = nsTime();
    int sum = 0;
    for (size_t i=0; i<N; i++) {
      sum += data[i];
    }
    int64 const b = nsTime();
    cout << "Simple loop (indexed): " << (b-a)/1e9 << "\n";
  }

  {
    int64 const a = nsTime();
    int sum = 0;
    T *d = data;
    for (size_t i=0; i<N; i++) {
      sum += *d++;
    }
    int64 const b = nsTime();
    cout << "Simple loop (pointer): " << (b-a)/1e9 << "\n";
  }

  {
    int64 const a = nsTime();
    int sum = 0;
    for (size_t i=0; i<N; i++) {
      int a = sum+3;
      int b = 4-sum;
      int c = sum+5;
      sum += data[i] + a - b + c;
    }
    int64 const b = nsTime();
    cout << "Loop that uses more ALUs (indexed): " << (b-a)/1e9 << "\n";
  }

  {
    int64 const a = nsTime();
    int sum = 0;
    T *d = data;
    for (size_t i=0; i<N; i++) {
      int a = sum+3;
      int b = 4-sum;
      int c = sum+5;
      sum += *d++ + a - b + c;
    }
    int64 const b = nsTime();
    cout << "Loop that uses more ALUs (pointer): " << (b-a)/1e9 << "\n";
  }
}

W systemie fast Core 2 (g++ 4.1.2, x64), oto czas:

    Simple loop (indexed): 0.400842
    Simple loop (pointer): 0.380633
    Loop that uses more ALUs (indexed): 0.768398
    Loop that uses more ALUs (pointer): 0.777886

Czasem indeksowanie jest szybsze, czasem arytmetyka wskaźników. Zależy to od tego, jak procesor i kompilator są w stanie rurociągu wykonywania pętli.

 4
Author: Mr Fooz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-10-24 12:25:43

Jeśli masz do czynienia z danymi typu array, powiedziałbym, że użycie indeksów dolnych sprawia, że kod jest bardziej czytelny. Na dzisiejszych maszynach (szczególnie w przypadku czegoś prostego) czytelny kod jest ważniejszy.

Teraz jeśli masz do czynienia jawnie z kawałkiem danych, które malloc () ' d i chcesz dostać wskaźnik wewnątrz tych danych, powiedzmy 20 bajtów w nagłówku pliku audio, to myślę, że arytmetyka adresów wyraźniej wyraża to, co próbujesz zrobić.

nie jestem pewien co do kompilatora optymalizacje w tym zakresie, ale nawet jeśli subskrypcja jest wolniejsza, jest wolniejsza tylko o może kilka cykli zegara co najwyżej. To prawie nic, kiedy możesz zyskać o wiele więcej dzięki jasności twojego pociągu myśli.

EDIT: zgodnie z niektórymi innymi odpowiedziami, subskrybowanie jest tylko elementem składniowym i nie ma wpływu na wydajność, jak myślałem. W takim przypadku zdecydowanie wybieraj kontekst, który próbujesz wyrazić za pomocą danych dostępowych wewnątrz wskazywanego bloku za wskaźnik.

 3
Author: Bob Somers,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-10-24 11:37:06

Należy pamiętać, że szybkość wykonania jest trudna do przewidzenia, nawet jeśli patrzymy na kod maszynowy z procesorami supersalarowymi i tym podobnymi z

  • out of order exection
  • pipelining
  • przewidywanie gałęzi
  • hyperthreading
  • ...

To nie tylko instrukcje liczenia maszyn i nie tylko liczenie zegarków. Wydaje się łatwiej po prostu zmierzyć w przypadkach, gdy naprawdę konieczne. Nawet jeśli nie jest niemożliwe obliczenie prawidłowego cyklu licz na dany program (musieliśmy to zrobić na studiach), ale to nie jest zabawne i trudno się dobrze dostać. Uwaga: prawidłowy pomiar jest również trudny w środowiskach wielowątkowych / wielowątkowych.

 3
Author: TheMarko,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-10-24 12:18:38
char p1[ ] = "12345";
char* p2 = "12345";

char *ch = p1[ 3 ]; /* 4 */
ch = *(p2 + 3); /* 4 */

Standard C nie mówi, który jest szybszy. Na obserwowalne zachowanie jest takie samo i to do kompilatora zaimplementować go w dowolny sposób chce. Często nawet nie czyta pamięci.

Ogólnie rzecz biorąc, nie można powiedzieć, który jest "szybszy", chyba że podasz kompilator, wersję, architekturę i opcje kompilacji. Nawet wtedy Optymalizacja będzie zależeć od otaczającego kontekstu.

Więc ogólną radą jest użycie tego, co daje jaśniejszy i prostszy kod. Używanie tablic [i] daje niektórym narzędziom możliwość znalezienia warunków indeksu-out-of-bound, więc jeśli używasz tablic, lepiej je traktować jako takie.

Jeśli jest krytyczna-zajrzyj do assemblera, który generuje kompilator. Ale pamiętaj, że może się to zmienić, gdy zmienisz kod, który go otacza.

 1
Author: n-alexander,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-10-24 12:00:36

Nie, używanie arytmetyki wskaźnika nie jest szybsze i prawdopodobnie wolniejsze, ponieważ kompilator optymalizujący może używać instrukcji takich jak LEA (Load Effective Address) na procesorach Intela lub podobnych na innych procesorach do arytmetyki wskaźnika, która jest szybsza niż add lub add/mul. Ma tę zaletę, że robi kilka rzeczy na raz i nie wpływa na flagi, a także zajmuje jeden cykl do obliczenia. BTW, poniżej jest z podręcznika GCC. Tak więc -Os nie optymalizuje przede wszystkim prędkości.

I również całkowicie zgadzam się z Marko. Najpierw spróbuj napisać czysty, czytelny i wielokrotnego użytku kod, a następnie pomyśl o optymalizacji i użyj narzędzi do profilowania, aby znaleźć wąskie gardło. Przez większość czasu problem z wydajnością jest związany z I/O lub jakimś złym algorytmem lub jakimś błędem, który musisz wytropić. Knuth to człowiek; -)

Przyszło mi do głowy, że co zrobisz z tablicą struktury. Jeśli chcesz zrobić arytmetykę wskaźnika, to zdecydowanie powinieneś to zrobić dla każdego członka struktura. Czy to brzmi jak przesada? Tak, oczywiście to przesada, a także otwiera szerokie drzwi do niejasnych błędów.

-Os optymalizacja pod kątem rozmiaru. Os włącza wszystkie optymalizacje O2, które zazwyczaj nie zwiększają rozmiaru kodu. Wykonuje również dalsze optymalizacje mające na celu zmniejszenie rozmiaru kodu.

 1
Author: Malkocoglu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-03-19 15:04:59

To nieprawda. Jest dokładnie tak szybki jak w przypadku operatorów indeksów dolnych. W Objective-C można używać tablic, jak w C i w stylu obiektowym, gdzie styl obiektowy jest o wiele wolniejszy, ponieważ wykonuje pewne operacje w każdym wywołaniu ze względu na dynamiczny charakter wywołania.

 1
Author: JtR,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-03-19 15:06:02

Jest mało prawdopodobne, że będzie jakaś różnica w prędkości.

Używanie operatora tablicy [] jest prawdopodobnie preferowane, ponieważ w C++ można używać tej samej składni z innymi kontenerami (np. vector).

 0
Author: MrZebra,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-10-24 11:33:12

Pracowałem nad optymalizacją C++/assembly dla kilku tytułów AAA przez 10 lat i mogę powiedzieć, że na poszczególnych platformach/kompilatorze pracowałem nad arytmetyka wskaźników zrobiła całkiem wymierną różnicę.

Jako przykład dla spojrzenia na sytuację, byłem w stanie zrobić naprawdę ciasną pętlę w naszym generatorze cząstek o 40% szybciej, zastępując cały dostęp do tablicy arytmetyką wskaźników do całkowitego niedowierzania moim współpracownikom. Słyszałem o tym od jednego z moich nauczyciele jako dobry trick z powrotem w dzień, ale założyłem, że nie będzie sposobu, aby to zrobić różnicę z kompilatorów / CPU mamy dzisiaj. Myliłem się;)

Należy podkreślić, że wiele procesorów konsoli ARM nie posiada wszystkich uroczych cech nowoczesnychCISC procesorów i kompilatora było czasami trochę chwiejnych.

 0
Author: Code Herder,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-05-22 11:23:17