Java'da sınırlandırılmış bir Dizeyi bölmenin en hızlı yolu


10

Sınırlandırılmış bir dize üzerinde çok sütunlu sıralama yeteneği sağlayan bir karşılaştırıcı inşa ediyorum. Şu anda ham String belirteçleri bölme için tercih edilen seçim olarak String sınıfından split yöntemini kullanıyorum.

Bu, raw String'i bir String dizisine dönüştürmenin en iyi performans yolu mu? Milyonlarca satırı sıralayacağım, bu yüzden yaklaşımın önemli olduğunu düşünüyorum.

İyi çalışıyor gibi görünüyor ve çok kolay, ama java'da daha hızlı bir yol olup olmadığından emin değilim.

Benim karşılaştırıcıda sıralama şu şekilde çalışır:

public int compare(String a, String b) {

    String[] aValues = a.split(_delimiter, _columnComparators.length);
    String[] bValues = b.split(_delimiter, _columnComparators.length);
    int result = 0;

    for( int index : _sortColumnIndices ) {
        result = _columnComparators[index].compare(aValues[index], bValues[index]);
        if(result != 0){
            break;
        }
    }
    return result;
}

İster inanın ister inanmayın çeşitli yaklaşımları karşılaştırdıktan sonra, split yöntemi java'nın en son sürümünü kullanan en hızlı yöntemdi. Tamamladığım karşılaştırıcıyı buradan indirebilirsiniz: https://sourceforge.net/projects/multicolumnrowcomparator/


5
Bu sorunun cevabının niteliğinin jvm'nin uygulanmasına bağlı olduğuna dikkat çekeceğim. Dizelerin davranışı (ortak bir destek dizisini OpenJDK'da paylaşır, ancak OracleJDK'da paylaşmaz) farklıdır. Bu farkın, çöp toplama ve bellek sızıntılarının yanı sıra bölme dizeleri ve alt dizelerin oluşturulması üzerinde önemli etkileri olabilir. Bu diziler ne kadar büyük? Şimdi nasıl yapıyorsun? Gerçek Java Dizeleri yerine yeni bir Stringish türü oluşturan bir yanıtı düşünür müsünüz?

1
Özellikle sonunda package private String yapıcısını çağıran StringTokenizer nextToken'e bakın . Bunu Java'da yapılan Dize İç Temsilde Değişiklikler bölümünde belgelenen değişikliklerle karşılaştırın 1.7.0_06

Dizi boyutu sütun sayısına bağlıdır, bu yüzden değişkendir. Bu çok sütunlu Karşılaştırıcı şöyle bir parametre olarak iletilir: ExternalSort.mergeSortedFiles (fileList, new File ("BigFile.csv"), _comparator, Charset.defaultCharset (), false); Harici sıralama rutini tüm satır dizesini sıralar, aslında sıralama sütunlarına göre bölme ve sıralama yapan karşılaştırıcıdır
Constantin

Lucene'nin belirteçlerine bakmayı düşünürdüm. Lucene, hem basit hem de karmaşık görevler için iyi performans gösteren güçlü bir metin analiz kütüphanesi olarak kullanılabilir
Doug T.

Apache Commons Lang's'ı düşünün StringUtils.split[PreserveAllTokens](text, delimiter).
Monica

Yanıtlar:


19

Bunun için hızlı ve kirli bir kıyaslama testi yazdım. Bazıları bölünmekte olan veriler hakkında özel bilgi gerektiren 7 farklı yöntemi karşılaştırır.

Temel genel amaçlı bölme için, Guava Splitter String # split () 'den 3.5x daha hızlıdır ve bunu kullanmanızı tavsiye ederim. Stringtokenizer bundan biraz daha hızlıdır ve indexOf ile bölünmek tekrar iki kat daha hızlıdır.

Kod ve daha fazla bilgi için bkz. Http://demeranville.com/battle-of-the-tokenizers-delimited-text-parser-performance/


Sadece hangi JDK'yı kullandığınızı merak ediyorum ... ve 1.6 olsaydı, 1.7'de sonuçlarınızın bir özetini görmek isterdim.

1
sanırım 1.6 idi. 1.7'de çalıştırmak istiyorsanız, kod bir JUnit testi olarak bulunur. Not String.split, tanımlanmış tek bir karaktere bölünmekten her zaman daha yavaş olacak normal ifade eşleştirmesi gerçekleştirir.
tom

1
Evet, ancak 1.6 için StringTokenizer (ve benzeri) kodu, aynı destek dizisini kullanarak yeni dizenin O (1) oluşturulmasını sağlayan bir String.substring () çağırır. Bu, O (n) yerine destek dizisinin gerekli bölümünün bir kopyasını yapmak için 1.7'de değiştirildi. Bu, sonuçlarınızda split ve StringTokenizer arasındaki farkı daha az hale getirerek (daha önce alt dize kullanılan her şeyi yavaşlatır) sonuçlarınızın tekil bir etkisi olabilir.

1
Kesinlikle doğru. Mesele, StringTokenizer'in çalıştığı yoldan "yeni bir dize oluşturmak için 3 tamsayı ata" dan "yeni bir dize oluşturmak, verilerin bir dizi kopyasını yapmak" a gitmesi, bu parçanın ne kadar hızlı olduğunu değiştirecektir. Çeşitli yaklaşımlar arasındaki fark şimdi daha az olabilir ve Java 1.7 ile takip yapmak ilginçtir (ilginç olmaktan başka bir sebep olmasa bile).

1
Bu yazı için teşekkürler! Çok kullanışlı ve çeşitli yaklaşımları karşılaştırmak için kullanacağız.
Constantin

5

@Tom'un yazdığı gibi, indexOf türü bir yaklaşım daha hızlıdır String.split(), çünkü ikincisi düzenli ifadelerle ilgilenir ve onlar için çok fazla ek yüke sahiptir.

Ancak, size süper bir hız kazandırabilecek bir algoritma değişikliği. Bu Karşılaştırıcının ~ 100.000 Dizginizi sıralamak için kullanılacağını varsayarak, yazmayın Comparator<String>. Çünkü, sıralamanız sırasında, aynı Dize muhtemelen birden çok kez karşılaştırılacaktır , bu yüzden birden çok kez bölebilirsiniz vb.

Tüm Dizeleri bir kez String [] s'ye bölün ve bir Comparator<String[]>String [] dizisine sahip olun . Sonra, sonunda, hepsini bir araya getirebilirsiniz.

Alternatif olarak, Dize -> Dize [] öğelerini önbelleğe almak için bir Harita da kullanabilirsiniz. örneğin (kabataslak) Ayrıca, hız için bellek ticareti yapıyorsunuz, umarım lotsa RAM'iniz var

HashMap<String, String[]> cache = new HashMap();

int compare(String s1, String s2) {
   String[] cached1 = cache.get(s1);
   if (cached1  == null) {
      cached1 = mySuperSplitter(s1):
      cache.put(s1, cached1);
   }
   String[] cached2 = cache.get(s2);
   if (cached2  == null) {
      cached2 = mySuperSplitter(s2):
      cache.put(s2, cached2);
   }

   return compareAsArrays(cached1, cached2);  // real comparison done here
}

bu iyi bir nokta.
tom

Burada bulunan Harici Sıralama kodunda değişiklik yapılması gerekir: code.google.com/p/externalsortinginjava
Constantin

1
O zaman bir Harita kullanmak muhtemelen en kolay. Bkz. Düzenleme.
user949300

Bu harici bir sıralama motorunun bir parçası olduğu göz önüne alındığında (muhtemelen mevcut belleğe sığabileceğinden çok daha fazla veri ile uğraşmak için), gerçekten verimli bir "ayırıcı" peşindeydim (evet, aynı dizeyi tekrar tekrar bölmek israf, dolayısıyla benim Orijinal bunu olabildiğince hızlı yapması gerekir)
Constantin

ExternalSort koduna kısaca göz attığınızda, her sortAndSave()aramanın sonunda (veya başlangıcında) önbelleğinizi temizlediyseniz, büyük bir önbellek nedeniyle belleğiniz tükenmemelidir. IMO, kodun olayları tetiklemek veya sizin gibi kullanıcıların geçersiz kılabileceği hiçbir şey korumalı yöntemleri çağırmak gibi birkaç kancaya sahip olması gerekir. (Onlar böylece Ayrıca, tüm statik yöntemlerini olmamalı yapmak için yazarlarla iletişim ve bir istemde bulunmak isteyebilirsiniz bu).
user949300

2

Bu kıyaslamalara göre , StringTokenizer dizeleri bölmek için daha hızlıdır, ancak daha az kullanışlı hale getiren bir dizi döndürmez.

Milyonlarca satırı sıralamanız gerekiyorsa RDBMS kullanmanızı öneririm.


3
Bu JDK 1.6'nın altındaydı - dizelerdeki şeyler 1.7'de temel olarak farklı - bkz. Java-performance.info/changes-to-string-java-1-7-0_06 (özellikle, bir alt dize oluşturmak artık O (1) değil ama bunun yerine O (n)). Bağlantı, 1.6 Pattern.split dosyasında String.substring ()) 'den farklı bir String oluşturma kullandığını belirtiyor - StringTokenizer.nextToken () ve erişimine sahip olduğu paket özel kurucusunu takip etmek için yukarıdaki açıklamada verilen koda bakın.

1

Sekmeyle ayrılmış büyük (1GB +) dosyaları ayrıştırmak için kullandığım yöntem budur. Daha az yükü vardır String.split(), ancak charsınırlayıcı olarak sınırlıdır . Birinin daha hızlı bir yöntemi varsa, görmek isterim. Bu aynı zamanda yapılabilir CharSequenceve CharSequence.subSequenceancak uygulanması gerekir CharSequence.indexOf(char)( String.indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)ilgileniyorsa paket yöntemine bakın ).

public static String[] split(final String line, final char delimiter)
{
    CharSequence[] temp = new CharSequence[(line.length() / 2) + 1];
    int wordCount = 0;
    int i = 0;
    int j = line.indexOf(delimiter, 0); // first substring

    while (j >= 0)
    {
        temp[wordCount++] = line.substring(i, j);
        i = j + 1;
        j = line.indexOf(delimiter, i); // rest of substrings
    }

    temp[wordCount++] = line.substring(i); // last substring

    String[] result = new String[wordCount];
    System.arraycopy(temp, 0, result, 0, wordCount);

    return result;
}

Bunu vs String.split () ile karşılaştırdınız mı? Öyleyse, nasıl karşılaştırır?
Jay Elston

@JayElston 900 MB'lık bir dosyada, bölünme süresini 7.7 saniyeden 6.2 saniyeye düşürdü, böylece yaklaşık% 20 daha hızlı. Bu hala kayan noktalı matris ayrıştırma işlemimin en yavaş kısmı. Kalan zamanın çoğunun dizi ayırma olduğunu tahmin ediyorum. Metodda bir ofset ile bir tokenizer tabanlı yaklaşım kullanarak matris tahsisini kesmek mümkün olabilir - bu daha çok kodun üzerinde belirttiğim yönteme benzemeye başlayacaktır.
vallismortis
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.