2009-11-04 11 views
11

Chcę przejść przez każdy znak w ciągu i przekazać każdy znak ciągu jako ciąg do innej funkcji.charAt() lub podłańcuch? Który jest szybszy?

String s = "abcdefg"; 
for(int i = 0; i < s.length(); i++){ 
    newFunction(s.substring(i, i+1));} 

lub

String s = "abcdefg"; 
for(int i = 0; i < s.length(); i++){ 
    newFunction(Character.toString(s.charAt(i)));} 

Wynik końcowy musi być ciągiem. Więc jakikolwiek pomysł, który będzie szybszy lub bardziej wydajny?

Odpowiedz

15

Jak zwykle: to nie ma znaczenia, ale jeśli nalegać na spędzanie czasu na mikro optymalizacji lub jeśli naprawdę chcesz, aby zoptymalizować za szczególny przypadek użycia, spróbuj tego:

import org.junit.Assert; 
import org.junit.Test; 

public class StringCharTest { 

    // Times: 
    // 1. Initialization of "s" outside the loop 
    // 2. Init of "s" inside the loop 
    // 3. newFunction() actually checks the string length, 
    // so the function will not be optimized away by the hotstop compiler 

    @Test 
    // Fastest: 237ms/562ms/2434ms 
    public void testCacheStrings() throws Exception { 
     // Cache all possible Char strings 
     String[] char2string = new String[Character.MAX_VALUE]; 
     for (char i = Character.MIN_VALUE; i < Character.MAX_VALUE; i++) { 
      char2string[i] = Character.toString(i); 
     } 

     for (int x = 0; x < 10000000; x++) { 
      char[] s = "abcdefg".toCharArray(); 
      for (int i = 0; i < s.length; i++) { 
       newFunction(char2string[s[i]]); 
      } 
     } 
    } 

    @Test 
    // Fast: 1687ms/1725ms/3382ms 
    public void testCharToString() throws Exception { 
     for (int x = 0; x < 10000000; x++) { 
      String s = "abcdefg"; 
      for (int i = 0; i < s.length(); i++) { 
       // Fast: Creates new String objects, but does not copy an array 
       newFunction(Character.toString(s.charAt(i))); 
      } 
     } 
    } 

    @Test 
    // Very fast: 1331 ms/ 1414ms/3190ms 
    public void testSubstring() throws Exception { 
     for (int x = 0; x < 10000000; x++) { 
      String s = "abcdefg"; 
      for (int i = 0; i < s.length(); i++) { 
       // The fastest! Reuses the internal char array 
       newFunction(s.substring(i, i + 1)); 
      } 
     } 
    } 

    @Test 
    // Slowest: 2525ms/2961ms/4703ms 
    public void testNewString() throws Exception { 
     char[] value = new char[1]; 
     for (int x = 0; x < 10000000; x++) { 
      char[] s = "abcdefg".toCharArray(); 
      for (int i = 0; i < s.length; i++) { 
       value[0] = s[i]; 
       // Slow! Copies the array 
       newFunction(new String(value)); 
      } 
     } 
    } 

    private void newFunction(String string) { 
     // Do something with the one-character string 
     Assert.assertEquals(1, string.length()); 
    } 

} 
+0

W miarę przesuwania tego ciągu należy zmienić nieco test w pierwszym teście. {char [] s = "abcdefg" .toCharArray();} powinno być wewnątrz pętli lub nawet lepiej (aby zapobiec sprytnej optymalizacji przez maszynę JVM, umieść całą pętlę i .toCharArray(), w oddzielnej funkcji). Ważne jest zmierzenie wszystkich początkowych kosztów ogólnych, a także kosztów pętli. Zwłaszcza, że ​​wydajność może realistycznie przechodzić od jednego do drugiego w oparciu o długość struny. Dlatego ważne jest również testowanie różnych długości użądlenia. – MatBailie

+5

+1 za rzeczywiste udzielenie odpowiedzi na pytanie. – gustafc

+0

Przeniesiono "s" wewnątrz pętli i dodano assert(), aby zapobiec optymalizacji JVM newFunction(). Oczywiście jest teraz wolniej, ale względne pomiary są nadal takie same. Chodzi mi tylko o to, że istnieją możliwości optymalizacji, jeśli problem jest dokładnie znany. Nie chodzi o to, aby zmienić funkcję, która ma być używana dla określonej operacji, ale aby zobaczyć operację na wyższym poziomie, aby uzyskać ulepszenia, np. przez buforowanie – mhaller

4

Czy naprawdę trzeba wziąć ? Byłoby lepiej, gdyby można zrobić newFunction wziąć char i nazwać tak:

newFunction(s.charAt(i)); 

W ten sposób można uniknąć tworzenia tymczasowego obiektu String.

Aby odpowiedzieć na pytanie: Trudno powiedzieć, który z nich jest bardziej wydajny. W obu przykładach należy utworzyć obiekt String, który zawiera tylko jeden znak. Co jest bardziej wydajne, zależy od tego, jak dokładnie są implementowane String.substring(...) i Character.toString(...) w danej implementacji Java. Jedynym sposobem, aby go znaleźć, jest uruchomienie programu za pomocą profilera i sprawdzenie, która wersja wykorzystuje więcej procesora i/lub więcej pamięci. Zwykle nie powinieneś martwić się o mikrooptymalizację, taką jak ta - poświęć na to tylko chwilę, gdy odkryjesz, że jest to przyczyną problemów z wydajnością i/lub pamięcią.

+0

newFunction naprawdę musi weź ciąg znaków. Oprócz pojedynczych znaków newFunction obsługuje również dłuższe łańcuchy. I obsługuje je w ten sam sposób. Nie chcę przeciążać newFunction, aby wziąć char, ponieważ robi to samo w obu przypadkach. – estacado

+1

Zgadzam się całkowicie, że mikrooptymalizacji należy unikać w rozwoju, dopóki nie okaże się to konieczne. Uważam także, że jako ćwiczenie edukacyjne bardzo ważne jest poznanie przydziałów pamięci i innych "ukrytych zachowań". Jestem osobiście zmęczony krnąbrnymi programistami, którzy wybijają krótki kod w przekonaniu, że short = performant i nieświadomie używają wysoce nieefektywnych algorytmów. Ludzie, którzy się tego nie uczą = leniwi. Ludzie, którzy są przez to unieruchomieni = wolno. Musi być równowaga. Moim zdaniem :) – MatBailie

+0

@estacado: Jeśli wydajność jest dla Ciebie kierowcą (jak sugeruje twój post), optymalizuj w odpowiednich miejscach. Przeciążanie nowej funkcji w celu uniknięcia narzutu String -may- będzie rozsądną opcją w zależności od tego, jak wyglądałaby wersja [char]. Skracanie kodu wokół funkcji może być bardziej czasochłonne, mniej skuteczne i mniej łatwe w utrzymaniu. – MatBailie

15

Odpowiedź brzmi: it doesn't matter.

Wpisz swój kod. Czy to twoje wąskie gardło?

+0

Profil w jaki sposób? Do wykorzystania pamięci? –

0

Najpierw uzyskałbym bazowy char [] z łańcucha źródłowego za pomocą String.toCharArray(), a następnie do wywołania newFunction.

Ale zgadzam się z Jesper że najlepiej byłoby, gdyby po prostu do czynienia z postaciami i uniknąć wszystkich funkcji String ...

+0

String.charAt (i) robi to wyszukiwanie, o ile wiem. Kopiowanie ciągu znaków do nowej tablicy (co rozumiem jako String.toCharArray()) wprowadza nową i inną narzut. Czy wielokrotnie przekazywanie odwołania do łańcucha znaków charAt() jest wolniejsze niż konwersja do macierzy rodzimej? Podejrzewam, że zależy to od długości struny ... – MatBailie

+0

Zawsze są kompromisy :) Tylko OP może naprawdę powiedzieć, co jest bardziej wydajne. –

2

Spośród dwóch fragmentów już opublikowanych, nie chcą mówić. Zgadzam się z Willem, że prawie na pewno nie ma to znaczenia dla ogólnej wydajności twojego kodu - a jeśli tak nie jest, możesz po prostu wprowadzić zmianę i ustalić dla siebie, które jest najszybsze dla danych z JVM na twoim sprzęcie.

To powiedziawszy, prawdopodobne jest, że drugi fragment byłby lepszy, gdyby najpierw przekonwertować ciąg na tablicę znaków, a następnie wykonać iteracje na macierzy. Robienie tego w ten sposób wykonywałoby tylko narzutu String tylko raz (konwersja do tablicy) zamiast każdego wywołania. Dodatkowo, możesz przekazać tablicę bezpośrednio do konstruktora String z pewnymi indeksami, co jest bardziej wydajne niż odebranie macierzy do przekazania jej pojedynczo (która następnie zostaje przekształcona w tablicę jednoznakową):

String s = "abcdefg"; 
char[] chars = s.toCharArray(); 
for(int i = 0; i < chars.length; i++) { 
    newFunction(String.valueOf(chars, i, 1)); 
} 

Ale aby wzmocnić mój pierwszy punkt, kiedy patrzysz na to, czego tak naprawdę unikasz przy każdym wywołaniu String.charAt() - to są dwa sprawdzenia graniczne, (leniwy) Boolowski OR i dodatek. Nie spowoduje to zauważalnej różnicy. Ani nie jest różnica w konstruktorach String.

Zasadniczo, oba idiomy są w porządku pod względem wydajności (nie jest to oczywiście oczywiście nieefektywne), więc nie powinieneś poświęcać więcej czasu na pracę nad nimi, chyba że profiler wykaże, że zajmuje to dużą ilość środowiska wykonawczego twojej aplikacji.I nawet wtedy prawie na pewno można uzyskać większy wzrost wydajności poprzez restrukturyzację kodu pomocniczego w tym obszarze (na przykład, aby newFunction potraktować cały ciąg); java.lang.String jest dość dobrze zoptymalizowany pod tym kątem.

+0

'substring' w bieżącym jvm faktycznie używa oryginalnej tablicy znaków jako zaplecza, podczas gdy ty inicjujesz kopię. Tak więc moje odczucia mówią, że podciągi będą faktycznie szybsze, ponieważ memcpy będą prawdopodobnie droższe (w zależności od tego, jak duży jest ciąg, większy jest lepszy). – wds

Powiązane problemy