2013-07-10 11 views
12

Niedawno przeprowadziłem mały projekt badawczy dotyczący wydajności sekwencyjnego lub losowego dostępu do NSArray w odniesieniu do C Array. Większość przypadków testowych pojawia się tak, jak się spodziewałem, jednak kilka z nich nie działa tak, jak myślałem, i mam nadzieję, że ktoś może być w stanie wyjaśnić dlaczego.Porównywanie wydajności NSArray vs C Array

Zasadniczo test polega na wypełnieniu tablicy C obiektami 50k, iteracji po każdym z nich i wywołaniu metody (która wewnętrznie po prostu zwiększa wartość zmiennoprzecinkową w obiekcie), druga część testu polega na utworzeniu pętli, która kończy 50k iteracje, ale uzyskuje dostęp do losowego obiektu w tablicy. Zasadniczo jest to dość proste.

Aby wykonać porównanie, inicjalizuję NSArray przy pomocy C Array. Każdy test jest następnie uruchamiany za pośrednictwem bloku przekazywanego do metody, która śledzi czas potrzebny na wykonanie bloku. Kod, którego używam, jest zawarty poniżej, ale chciałbym omówić wyniki i zapytania, które najpierw mam.

Te testy zostały uruchomione na iPhone 4 i owinięte w dispatch_after, aby złagodzić wszelkie pozostałe wątki lub operacje niepodzielone w wyniku uruchomienia aplikacji. Wyniki jednym cyklu są następujące, każdy prowadzony jest w zasadzie to samo z niewielkimi zmianami:

===SEQUENCE=== 
NSARRAY FAST ENUMERATION: 12ms 
NSARRAY FAST ENUMERATION WEAK: 186ms 
NSARRAY BLOCK ENUMERATION: 31ms (258.3%) 
C ARRAY DIRECT: 7ms (58.3%) 
C ARRAY VARIABLE ASSIGN: 33ms (275.0%) 
C ARRAY VARIABLE ASSIGN WEAK: 200ms (1666.7%) 

===RANDOM=== 
NSARRAY RANDOM: 102ms (850.0%) *Relative to fast enumeration 
C ARRAY DIRECT RANDOM: 39ms (38.2%) *Relative to NSArray Random 
C ARRAY VARIABLE ASSIGN RANDOM: 82ms (80.4%) 

Najszybszy podejście wydaje się być bezpośrednio dostępu do elementów w tablicy C za pomocą „* (carray + IDX)” , jednak najbardziej zastanawiające jest to, że przypisanie wskaźnika z tablicy C do obiektywnej zmiennej c "id object = * (carry + idx)" powoduje ogromną wydajność.

Początkowo sądziłem, że być może łuk robił coś z liczeniem referencji, ponieważ zmienna była silna, więc w tym momencie zmieniłem ją na słabą oczekując, że wydajność zwiększy "__weak id object = * (carry + idx)". Ku mojemu zdziwieniu było to znacznie wolniej.

Wyniki losowego dostępu poszły całkiem nieźle, jak się spodziewałem na podstawie wyników sekwencji, więc nie ma żadnych niespodzianek na szczęście.

W wyniku tego istnieje szereg pytań:

  1. Dlaczego przypisanie do zmiennej trwa tak długo?
  2. Dlaczego przypisywanie do słabej zmiennej trwa jeszcze dłużej? (Być może jest coś, czego nie rozumiem, co się dzieje tutaj)
  3. Biorąc pod uwagę powyższe, w jaki sposób Apple ma standardowe szybkie wyliczenie, aby tak dobrze działać?

I dla kompletności tutaj jest kod. Więc jestem tworzenia tablic w następujący sposób:

__block id __strong *cArrayData = (id __strong *)malloc(sizeof(id) * ITEM_COUNT); 

for (NSUInteger idx = 0; idx < ITEM_COUNT; idx ++) { 
    NSTestObject *object = [[NSTestObject alloc] init]; 
    cArrayData[idx] = object; 
} 

__block NSArray *arrayData = [NSArray arrayWithObjects:cArrayData count:ITEM_COUNT]; 

I NSTestObject jest zdefiniowany następująco:

@interface NSTestObject : NSObject 

- (void)doSomething; 

@end 

@implementation NSTestObject 
{ 
    float f; 
} 

- (void)doSomething 
{ 
    f++; 
} 

A metoda stosowana do profilowania kodu:

int machTimeToMS(uint64_t machTime) 
{ 
    const int64_t kOneMillion = 1000 * 1000; 
    static mach_timebase_info_data_t s_timebase_info; 

    if (s_timebase_info.denom == 0) { 
     (void) mach_timebase_info(&s_timebase_info); 
    } 
    return (int)((machTime * s_timebase_info.numer)/(kOneMillion * s_timebase_info.denom)); 
} 

- (int)profile:(dispatch_block_t)call name:(NSString *)name benchmark:(int)benchmark 
{ 

    uint64_t startTime, stopTime; 
    startTime = mach_absolute_time(); 

    call(); 

    stopTime = mach_absolute_time(); 

    int duration = machTimeToMS(stopTime - startTime); 

    if (benchmark > 0) { 
     NSLog(@"%@: %i (%0.1f%%)", name, duration, ((float)duration/(float)benchmark) * 100.0f); 
    } else { 
     NSLog(@"%@: %i", name, duration); 
    } 

    return duration; 

} 

Wreszcie jest jak przeprowadzam aktualne testy:

int benchmark = [self profile:^ { 
    for (NSTestObject *view in arrayData) { 
     [view doSomething]; 
    } 
} name:@"NSARRAY FAST ENUMERATION" benchmark:0]; 

[self profile:^ { 
    for (NSTestObject __weak *view in arrayData) { 
     [view doSomething]; 
    } 
} name:@"NSARRAY FAST ENUMERATION WEAK" benchmark:0]; 

[self profile:^ { 
    [arrayData enumerateObjectsUsingBlock:^(NSTestObject *view, NSUInteger idx, BOOL *stop) { 
     [view doSomething]; 
    }]; 
} name:@"NSARRAY BLOCK ENUMERATION" benchmark:benchmark]; 

[self profile:^ { 
    for (NSUInteger idx = 0; idx < ITEM_COUNT; idx ++) { 
     [*(cArrayData + idx) doSomething]; 
    } 
} name:@"C ARRAY DIRECT" benchmark:benchmark]; 

[self profile:^ { 
    id object = nil; 
    NSUInteger idx = 0; 
    while (idx < ITEM_COUNT) { 
     object = (id)*(cArrayData + idx); 
     [object doSomething]; 
     object = nil; 
     idx++; 
    } 
} name:@"C ARRAY VARIABLE ASSIGN" benchmark:benchmark]; 

[self profile:^ { 
    __weak id object = nil; 
    NSUInteger idx = 0; 
    while (idx < ITEM_COUNT) { 
     object = (id)*(cArrayData + idx); 
     [object doSomething]; 
     object = nil; 
     idx++; 
    } 
} name:@"C ARRAY VARIABLE ASSIGN WEAK" benchmark:benchmark]; 

NSLog(@"\n===RANDOM===\n"); 

benchmark = [self profile:^ { 
    id object = nil; 
    for (NSUInteger idx = 0; idx < ITEM_COUNT; idx ++) { 
     object = arrayData[arc4random()%ITEM_COUNT]; 
     [object doSomething]; 
    } 
} name:@"NSARRAY RANDOM" benchmark:benchmark]; 

[self profile:^ { 
    NSUInteger idx = 1; 
    while (idx < ITEM_COUNT) { 
     [*(cArrayData + arc4random()%ITEM_COUNT) doSomething]; 
     idx++; 
    } 
} name:@"C ARRAY DIRECT RANDOM" benchmark:benchmark]; 

[self profile:^ { 
    id object = nil; 
    NSUInteger idx = 0; 
    while (idx < ITEM_COUNT) { 
     object = (id)*(cArrayData + arc4random()%ITEM_COUNT); 
     [object doSomething]; 
     idx++; 
    } 
} name:@"C ARRAY VARIABLE ASSIGN RANDOM" benchmark:benchmark]; 
+1

Wymagane czytanie: [Ryba z niedorzecznymi rybami: Array] (http://ridiculousfish.com/blog/posts/array.html) – Caleb

Odpowiedz

6

Dlaczego przypisywanie do zmiennej zajmuje tak dużo czasu?

Twoje przypuszczenie było poprawne: ARC wzywa retain podczas przypisywania i release po przeniesieniu, lub gdy id wykracza poza zakres.

Dlaczego przypisywanie do słabej zmiennej trwa jeszcze dłużej? (Może jest coś, czego nie rozumiem tutaj)

Przypomnij sobie, że ARC obiecuje oczyścić swój słaby punkt odniesienia, gdy zniknie ostatnie silne odniesienie. Dlatego słabe referencje są droższe: aby nil z adresu __weak id, ARC rejestruje adres id w środowisku wykonawczym, aby uzyskać powiadomienie o zwolnieniu obiektu. Ta rejestracja wymaga zapisania w tabeli mieszającej - znacznie wolniej niż tylko zachowanie i zwolnienie.

Biorąc pod uwagę powyższe, w jaki sposób firma Apple uzyskała standardowe szybkie wyliczenie, aby uzyskać tak dobre wyniki?

Szybkie wyliczanie korzysta z bloków tablicy, która bezpośrednio odpowiada NSArray. Zasadniczo pobierają one blok 30 elementów i traktują dostęp do niego jako zwykłą tablicę C. Następnie przechwytują następny blok, iterują go tak, jakby był tablicą C i tak dalej. Jest niewielki narzut, ale jest to jeden blok, a nie jeden element, więc masz imponującą wydajność.

+0

Dzięki za odpowiedź I rzeczywiście próbowałem chwytać bloki do 10 elementów w pętli i przypisywać je, ale miało to prawie zerowy wpływ na wydajność, ponieważ większość czasu pętli (~ 80%) spędza się na przypisaniu zmiennej. Jedyne wyjaśnienie, jakie mogę teraz zauważyć, to to, że szybkie wyliczanie zachowuje każdy blok obiektów i jakoś uwalnia je wszystkie na wątku tła, podczas gdy działa kolejny blok. Ale nie mam sposobu, aby to poprzeć. – Andy

+3

@Andy Nie sądzę, że szybkie wyliczanie zachowuje i zwalnia obiekty w ogóle: Myślę, że używają '__unsafe_unretained', ponieważ obiekt jest już własnością tablicy. – dasblinkenlight

+0

Ty jesteś moim bohaterem. Szybka zmiana __weaka na __unsafe_unretained i wydajność jest teraz 8 ms i szybsza niż NSArray. Nie zdawałem sobie sprawy, że przy korzystaniu z __unsafe_unretained istnieje taka korzyść, ale jak słusznie powtarzasz, odwołanie __weak musi ustawić wartość zerową, gdy zostanie usunięte z zakresu, więc ma to sens. Bardzo ci dziękuje za pomoc! – Andy

0

2), ponieważ nie ma możliwości optymalizacji. Słabe zmienne zawarte w statycznej pamięci i dostęp do statycznych dłuższych niż dynamiczne