Bir Java Dizesinden tüm yazdırılamayan karakterleri çıkarmanın en hızlı yolu


82

StringJava'da a'dan yazdırılamayan tüm karakterleri çıkarmanın en hızlı yolu nedir ?

Şimdiye kadar 138 bayt, 131 karakterlik String üzerinde denedim ve ölçtüm:

  • String'ler replaceAll()- en yavaş yöntem
    • 517009 sonuç / sn
  • Bir Kalıbı önceden derleyin, ardından Eşleştiriciyi kullanın replaceAll()
    • 637836 sonuç / sn
  • StringBuffer kullanın, codepointAt()tek tek kullanarak kod noktalarını alın ve StringBuffer'a ekleyin
    • 711946 sonuç / sn
  • StringBuffer kullanın, charAt()tek tek kullanarak karakterleri alın ve StringBuffer'a ekleyin
    • 1052964 sonuç / sn
  • Bir char[]tamponu önceden tahsis edin , charAt()tek tek kullanarak karakterleri alın ve bu tamponu doldurun, ardından tekrar String'e dönüştürün
    • 2022653 sonuç / sn
  • 2 char[]arabelleği önceden tahsis et - eski ve yeni, kullanarak mevcut String için tüm karakterleri bir kerede alın getChars(), eski arabelleği tek tek yineleyin ve yeni arabelleği doldurun, ardından yeni arabelleği String'e dönüştürün - kendi en hızlı sürümüm
    • 2502502 sonuç / sn
  • Sadece kullanarak - 2 tamponlarla aynı malzeme byte[], getBytes()ve "UTF-8" olarak kodlama belirten
    • 857485 sonuç / sn
  • 2 byte[]arabellekle aynı şeyler , ancak kodlamayı sabit olarak belirlemeCharset.forName("utf-8")
    • 791076 sonuç / sn
  • 2 byte[]arabellekle aynı şeyler , ancak kodlamayı 1 baytlık yerel kodlama olarak belirleme (yapılacak çok az mantıklı bir şey)
    • 370164 sonuç / sn

En iyi denemem şuydu:

    char[] oldChars = new char[s.length()];
    s.getChars(0, s.length(), oldChars, 0);
    char[] newChars = new char[s.length()];
    int newLen = 0;
    for (int j = 0; j < s.length(); j++) {
        char ch = oldChars[j];
        if (ch >= ' ') {
            newChars[newLen] = ch;
            newLen++;
        }
    }
    s = new String(newChars, 0, newLen);

Nasıl daha hızlı hale getirileceğine dair herhangi bir fikrin var mı?

Çok garip bir soruyu yanıtlamak için bonus puanlar: Neden "utf-8" karakter kümesi adını doğrudan kullanmak, önceden tahsis edilmiş statik sabit kullanmaktan daha iyi performans sağlar Charset.forName("utf-8")?

Güncelleme

  • Cırcır ucubesinden gelen öneri, etkileyici 3105590 sonuç / saniye performansı, +% 24'lük bir iyileşme sağlar!
  • Dan Öneri Ed Staub 3471017 sonuçlar / sn, önceki en fazla + 12% - Yine başka bir gelişme elde edilir.

Güncelleme 2

Önerilen tüm çözümleri ve bunların çapraz mutasyonlarını toplamak için elimden gelenin en iyisini yaptım ve github'da küçük bir kıyaslama çerçevesi olarak yayınladım . Şu anda 17 algoritma kullanıyor. Bunlardan biri "özel" - Voo1 algoritması ( SO kullanıcısı Voo tarafından sağlanır ) karmaşık yansıma hileleri kullanır, böylece yıldız hızları elde eder, ancak JVM dizgilerinin durumunu bozar , bu nedenle ayrı olarak değerlendirilir.

Kutunuzdaki sonuçları belirlemek için kontrol edebilir ve çalıştırabilirsiniz. İşte benimki ile ilgili aldığım sonuçların bir özeti. Özellikleri:

  • Debian sid
  • Linux 2.6.39-2-amd64 (x86_64)
  • Bir paketten yüklenen Java sun-java6-jdk-6.24-1, JVM kendini şu şekilde tanımlar:
    • Java (TM) SE Çalışma Zamanı Ortamı (derleme 1.6.0_24-b07)
    • Java HotSpot (TM) 64-Bit Sunucu VM (yapı 19.1-b02, karma mod)

Farklı algoritmalar, farklı bir girdi verisi kümesi verildiğinde sonuçta farklı sonuçlar gösterir. 3 modda bir kıyaslama yaptım:

Aynı tek dize

Bu mod, StringSourcesınıf tarafından bir sabit olarak sağlanan aynı tek dizede çalışır . Hesaplaşma:

 İşlem / s │ Algoritması
──────────┼──────────────────────────────
6535947 │ Voo1
──────────┼──────────────────────────────
5350454 │ RatchetFreak2EdStaub1GreyCat1
5 249 343 │ EdStaub1
5002501 │ EdStaub1GreyCat1
4 859086 │ ArrayOfCharFromStringCharAt
4295532 │ RatchetFreak1
4045 307 │ ArrayOfCharFromArrayOfChar
2790178 │ RatchetFreak2EdStaub1GreyCat2
2583 311 │ RatchetFreak2
1274 859 │ StringBuilderChar
1138174 │ StringBuilderCodePoint
  994 727 │ ArrayOfByteUTF8String
  918611 │ ArrayOfByteUTF8Const
  756086 │ Eşleştirici
  598945 │ StringReplaceAll
  46045 │ ArrayOfByteWindows1251

Çizelge halinde: (kaynak: greycat.ru )Aynı tek dize şeması

Çoklu dizeler, dizelerin% 100'ü kontrol karakterleri içerir

Kaynak dizge sağlayıcısı (0..127) karakter kümesi kullanarak çok sayıda rastgele dizeyi önceden oluşturdu - bu nedenle neredeyse tüm dizeler en az bir kontrol karakteri içeriyordu. Algoritmalar, bu önceden oluşturulmuş diziden sıralı dizilerle dizeler aldı.

 İşlem / s │ Algoritması
──────────┼──────────────────────────────
2 123142 │ Voo1
──────────┼──────────────────────────────
1782 214 │ EdStaub1
1776199 │ EdStaub1GreyCat1
1694 628 │ ArrayOfCharFromStringCharAt
1 481 481 │ ArrayOfCharFromArrayOfChar
1460 067 │ RatchetFreak2EdStaub1GreyCat1
1438435 │ RatchetFreak2EdStaub1GreyCat2
1 366 494 │ RatchetFreak2
1349710 │ RatchetFreak1
  893 176 │ ArrayOfByteUTF8String
  817 127 │ ArrayOfByteUTF8Const
  778 089 │ StringBuilderChar
  734 754 │ StringBuilderCodePoint
  377 829 │ ArrayOfByteWindows1251
  224 140 │ Eşleştirici
  211 104 │ StringReplaceAll

Çizelge halinde: (kaynak: greycat.ru )Çoklu dizeler,% 100 konsantrasyon

Çoklu dizeler, dizelerin% 1'i kontrol karakterleri içerir

Öncekiyle aynı, ancak dizelerin yalnızca% 1'i kontrol karakterleriyle oluşturuldu - diğer% 99'u [32..127] karakter seti kullanılarak oluşturuldu, bu nedenle hiçbir kontrol karakterleri içeremezler. Bu sentetik yük, benim yerime bu algoritmanın gerçek dünya uygulamasına en yakın olanıdır.

 İşlem / s │ Algoritması
──────────┼──────────────────────────────
3711 952 │ Voo1
──────────┼──────────────────────────────
2 851440 │ EdStaub1GreyCat1
2455796 │ EdStaub1
2426007 │ ArrayOfCharFromStringCharAt
2347969 │ RatchetFreak2EdStaub1GreyCat2
2 242152 │ RatchetFreak1
2171 553 │ ArrayOfCharFromArrayOfChar
1922707 │ RatchetFreak2EdStaub1GreyCat1
1857 010 │ RatchetFreak2
1023751 │ ArrayOfByteUTF8String
  939 055 │ StringBuilderChar
  907 194 │ ArrayOfByteUTF8Const
  841 963 │ StringBuilderCodePoint
  606465 │ Eşleştirici
  501 555 │ StringReplaceAll
  381 185 │ ArrayOfByteWindows1251

Çizelge halinde: (kaynak: greycat.ru )Çoklu diziler,% 1 konsantrasyon

En iyi cevabı kimin verdiğine karar vermek benim için çok zor, ancak gerçek dünyadaki en iyi çözümün Ed Staub tarafından verildiği / esinlendiği düşünüldüğünde, sanırım cevabını not etmek adil olur. Bunda yer alan herkese teşekkürler, görüşleriniz çok yardımcı ve paha biçilemezdi. Kutunuzda test paketini çalıştırmaktan ve daha iyi çözümler önermekten çekinmeyin (çalışan JNI çözümü, kimse var mı?).

Referanslar


21
"Bu soru araştırma çabasını gösteriyor" - hmm ... evet, geçer. +1
Gustav Barkefors

7
StringBuilderStringBuffersenkronize edilmediğinden marjinal olarak daha hızlı olacak , bunu etiketlediğiniz için micro-optimization

2
@Jarrod Roberson: Tamam, bu nedenle edelim tüm salt okunur nihai alanlar yapmak ve ayıklamak s.length()dışarı for:-) sıra döngü
Evde

3
Boşluğun altındaki bazı karakterler yazdırılabilir, örneğin \tve \n. 127'nin üzerindeki birçok karakter, karakter kümenizde yazdırılamaz.
Peter Lawrey

1
dize tamponunu kapasiteyle başlattınız s.length()mı?
ratchet freak

Yanıtlar:


11

Bu yöntemi iş parçacıkları arasında paylaşılmayan bir sınıfa yerleştirmek mantıklıysa, arabelleği yeniden kullanabilirsiniz:

char [] oldChars = new char[5];

String stripControlChars(String s)
{
    final int inputLen = s.length();
    if ( oldChars.length < inputLen )
    {
        oldChars = new char[inputLen];
    }
    s.getChars(0, inputLen, oldChars, 0);

vb...

Bu büyük bir kazanç - şu anki en iyi durumu anladığım kadarıyla% 20 ya da öylesine.

Bu, potansiyel olarak büyük dizelerde kullanılacaksa ve bellek "sızıntısı" bir sorunsa, zayıf bir referans kullanılabilir.


İyi fikir! Şimdiye kadar sayımları saniyede 3471017 dizgeye çıkardı - yani önceki en iyi sürüme göre +% 12'lik bir gelişme.
GreyCat

25

1 karakter dizisi kullanmak biraz daha iyi çalışabilir

int length = s.length();
char[] oldChars = new char[length];
s.getChars(0, length, oldChars, 0);
int newLen = 0;
for (int j = 0; j < length; j++) {
    char ch = oldChars[j];
    if (ch >= ' ') {
        oldChars[newLen] = ch;
        newLen++;
    }
}
s = new String(oldChars, 0, newLen);

ve tekrarlanan aramalardan kaçındım s.length();

işe yarayabilecek başka bir mikro optimizasyon

int length = s.length();
char[] oldChars = new char[length+1];
s.getChars(0, length, oldChars, 0);
oldChars[length]='\0';//avoiding explicit bound check in while
int newLen=-1;
while(oldChars[++newLen]>=' ');//find first non-printable,
                       // if there are none it ends on the null char I appended
for (int  j = newLen; j < length; j++) {
    char ch = oldChars[j];
    if (ch >= ' ') {
        oldChars[newLen] = ch;//the while avoids repeated overwriting here when newLen==j
        newLen++;
    }
}
s = new String(oldChars, 0, newLen);

1
Teşekkürler! Sürümünüz 3105590 dizi / sn verir - büyük bir gelişme!
GreyCat

newLen++;: ön artırmayı kullanmaya ne dersiniz ++newLen;? - ( ++jdöngüde de). Buraya bir göz atın: stackoverflow.com/questions/1546981/…
Thomas

finalBu algoritmaya ekleme yapmak ve kullanmak oldChars[newLen++]( ++newLenbir hatadır - tüm dizi 1 farkla kapalı olacaktır!) Ölçülebilir performans kazancı sağlamaz (yani, farklı çalışmaların farklılıklarıyla karşılaştırılabilir ±% 2,3 fark elde ederim)
GreyCat

@grey Diğer bazı optimizasyonlarla başka bir sürüm yaptım
ratchet freak

2
Hmm! Bu harika bir fikir! Üretim ortamımdaki dizelerin% 99,9'u gerçekten sökülmeyi gerektirmeyecek - ilk char[]ayırmayı bile ortadan kaldırmak için onu daha da geliştirebilir ve ayırma olmazsa Dizeyi olduğu gibi döndürebilirim.
GreyCat

11

Şu anki en iyi yöntemi (önceden tahsis edilmiş diziyle ucube çözümü), tedbirlerime göre yaklaşık% 30 oranında yendim. Nasıl? Ruhumu satarak.

Eminim şimdiye kadar tartışmayı takip eden herkes bunun hemen hemen tüm temel programlama ilkelerini ihlal ettiğini biliyor, ama pekala. Her neyse, aşağıdakiler yalnızca dizenin kullanılan karakter dizisi diğer dizeler arasında paylaşılmadığında çalışır - eğer hata ayıklamak zorunda kalırsa, bu sizi öldürmeye karar verme hakkına sahip olacaktır (substring () çağrısı olmadan ve bunu değişmez dizelerde kullanma) Bu, JVM'nin neden dışarıdan bir kaynaktan okunan benzersiz dizgeleri interneti kullandığını anlamadığım için çalışmalıdır). Yine de, kıyaslama kodunun bunu yapmadığından emin olmayı unutmayın - bu son derece muhtemeldir ve açık bir şekilde yansıtma çözümüne yardımcı olacaktır.

Neyse, işte başlıyoruz:

    // Has to be done only once - so cache those! Prohibitively expensive otherwise
    private Field value;
    private Field offset;
    private Field count;
    private Field hash;
    {
        try {
            value = String.class.getDeclaredField("value");
            value.setAccessible(true);
            offset = String.class.getDeclaredField("offset");
            offset.setAccessible(true);
            count = String.class.getDeclaredField("count");
            count.setAccessible(true);
            hash = String.class.getDeclaredField("hash");
            hash.setAccessible(true);               
        }
        catch (NoSuchFieldException e) {
            throw new RuntimeException();
        }

    }

    @Override
    public String strip(final String old) {
        final int length = old.length();
        char[] chars = null;
        int off = 0;
        try {
            chars = (char[]) value.get(old);
            off = offset.getInt(old);
        }
        catch(IllegalArgumentException e) {
            throw new RuntimeException(e);
        }
        catch(IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        int newLen = off;
        for(int j = off; j < off + length; j++) {
            final char ch = chars[j];
            if (ch >= ' ') {
                chars[newLen] = ch;
                newLen++;
            }
        }
        if (newLen - off != length) {
            // We changed the internal state of the string, so at least
            // be friendly enough to correct it.
            try {
                count.setInt(old, newLen - off);
                // Have to recompute hash later on
                hash.setInt(old, 0);
            }
            catch(IllegalArgumentException e) {
                e.printStackTrace();
            }
            catch(IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        // Well we have to return something
        return old;
    }

Benim testString için aldığını 3477148.18ops/sVS. 2616120.89ops/seski varyant için. Bunu yenmenin tek yolunun C dilinde yazmak (muhtemelen değil) ya da şimdiye kadar kimsenin düşünmediği tamamen farklı bir yaklaşım olabileceğinden eminim. Zamanlamanın farklı platformlarda kararlı olup olmadığından kesinlikle emin olmasam da - en azından kutumda (Java7, Win7 x64) güvenilir sonuçlar veriyor.


Çözüm için teşekkürler, lütfen soru güncellemesine göz atın - Test çerçevemi yayınladım ve 17 algoritma için 3 test çalıştırma sonucu ekledim. Algoritmanız her zaman en üsttedir, ancak Java String'in dahili durumunu değiştirir, dolayısıyla "immutable String" sözleşmesini bozar => gerçek dünya uygulamasında kullanmak oldukça zor olur. Test açısından, evet, bu en iyi sonuç, ama sanırım bunu ayrı bir aday olarak açıklayacağım :)
GreyCat

3
@GreyCat Evet, kesinlikle bazı büyük dizeler eklenmiş ve dürüst olmak gerekirse, sadece yazdım çünkü mevcut en iyi çözümünüzü daha da geliştirmenin göze çarpan bir yolu olmadığından eminim. İyi çalışacağından emin olduğum durumlar var (soymadan önce alt dize veya stajyer aramaları yok), ancak bunun nedeni mevcut bir Hotspot sürümü hakkındaki bilgiden kaynaklanıyor (ör. özellikle yararlı olun). Birinin gerçekten bu ekstra x% 'e ihtiyaç duyması faydalı olabilir, ancak aksi takdirde daha ne kadar geliştirebileceğinizi görmek için daha fazla bir temel;)
Voo

1
Zaman bulursam JNI versiyonunu denemeye çalışsam da - şimdiye kadar hiç kullanmadım, bu yüzden ilginç olurdu. Ancak, ek arama ek yükü (dizeler çok küçük) ve JIT'in işlevleri optimize etmekte bu kadar zorlanmaması nedeniyle daha yavaş olacağından oldukça eminim. İpinizin new String()değişmemesi durumunda kullanmayın , ama bence bunu zaten anladınız.
Voo

Zaten tamamen aynı şeyi saf C'de yapmaya çalıştım - ve bu, sizin yansıma tabanlı sürümünüze göre pek bir gelişme göstermiyor. C versiyonu +% 5..10 gibi bir şeyi daha hızlı çalıştırıyor, gerçekten o kadar da iyi değil - En azından 1.5x-1.7x gibi olacağını düşünmüştüm ...
GreyCat

2

İşlemcinin miktarına bağlı olarak görevi birkaç paralel alt göreve bölebilirsiniz.


Evet, ben de düşündüm, ama benim durumumda herhangi bir performans artışı sağlamayacak - bu sıyırma algoritması zaten büyük ölçüde paralel sistemde çağrılacaktı.
GreyCat

2
Ayrıca, her 50-100 baytlık dizide işlem yapmak için birkaç iş parçacığının ayrıştırılmasının çok büyük bir aşırılık olacağını tahmin edebilirim.
GreyCat

Evet, her küçük dizge için çatal ipliği kullanmak iyi bir fikir değildir. Ancak yük dengeleyici performansı artırabilir. BTW, performansı senkronize olduğu için performansı yetersiz olan StringBuffer yerine StringBuilder ile test ettiniz mi?
umbr

Üretim kurulumum, birkaç ayrı işlemi çalıştırıyor ve olabildiğince fazla paralel CPU ve çekirdek kullanıyor, böylece StringBuilderher yerde herhangi bir sorun olmadan özgürce kullanabiliyorum .
GreyCat

2

Çok özgürdüm ve farklı algoritmalar için küçük bir ölçüt yazdım. Mükemmel değil, ancak belirli bir algoritmanın minimum 1000 çalışmasını rastgele bir dizge üzerinden 10000 kez alıyorum (varsayılan olarak yaklaşık% 32/200 yazdırılamaz). Bu, GC, başlatma vb. Gibi şeylerle ilgilenmelidir - herhangi bir algoritmanın çok fazla engel olmadan en az bir çalıştırmaya sahip olmaması için çok fazla ek yük yoktur.

Özellikle iyi belgelenmemiş, ama çok iyi. İşte başlıyoruz - Hem cırcır ucube algoritmalarını hem de temel sürümü dahil ettim. Şu anda [0, 200) aralığında tekdüze dağıtılmış karakterlere sahip 200 karakter uzunluğunda bir dizgeyi rastgele başlatıyorum.


Çaba için +1 - ama bana sormalıydın - zaten benzer bir kıyaslama
paketim var

@GreyCat İyi olabilirdim, ama bunu bir araya getirmek (zaten var olan kodun dışında) muhtemelen daha hızlıydı;)
Voo

1

IANA düşük seviyeli java performans bağımlısı, ancak ana döngünüzü açmayı denediniz mi? Bazı CPU'ların paralel olarak kontroller yapmasına izin verebileceği anlaşılıyor.

Ayrıca bu , optimizasyonlar için bazı eğlenceli fikirlere sahiptir.


Burada herhangi bir açmanın yapılabileceğinden şüpheliyim, çünkü (a) önceki adımlarda aşağıdaki algoritma adımlarına bağımlılıklar var, (b) Java'da herhangi bir yıldız sonuçları üreten manuel döngü açma yapan hiç kimseyi duymadım; JIT, göreve uygun gördüğü her şeyi açmakta genellikle iyi bir iş çıkarır. Öneri ve bağlantı için teşekkürler :)
GreyCat

0

neden "utf-8" karakter kümesi adını doğrudan kullanmak önceden ayrılmış statik sabit karakter kümesi.forName ("utf-8") kullanmaktan daha iyi performans sağlar?

String#getBytes("utf-8")Şunu kastediyorsanız : Bu daha hızlı olmamalıdır - daha iyi önbelleğe alma dışında - çünkü Charset.forName("utf-8")karakter kümesi önbelleğe alınmamışsa dahili olarak kullanılır.

Bir şey, farklı karakter kümeleri kullanıyor olmanız olabilir (veya kodunuzun bir kısmı şeffaf bir şekilde çalışıyor olabilir), ancak önbelleğe alınan karakter kümesi StringCodingdeğişmez.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.