C ++ 'da verimli dize birleştirme


108

Birkaç kişinin std :: string'de "+" operatörü ve birleştirme işlemini hızlandırmak için çeşitli geçici çözümler hakkında endişelerini dile getirdiğini duydum. Bunlardan herhangi biri gerçekten gerekli mi? Öyleyse, dizeleri C ++ 'da birleştirmenin en iyi yolu nedir?


13
Temel olarak +, bir bitiştirme operatörü DEĞİLDİR (yeni bir dize oluşturduğu için). Birleştirme için + = kullanın.
Martin York

1
C ++ 11'den beri önemli bir nokta var: Operatör +, işlenenlerinden birini değiştirebilir ve eğer bu işlenen rvalue referansı ile aktarıldıysa onu hareket halinde döndürebilir. libstdc++ örneğin bunu yapar . Dolayısıyla, geçici olarak operatörü + çağırırken, neredeyse iyi performansa ulaşabilir - belki de okunabilirlik uğruna, bir darboğaz olduğunu gösteren kıyaslamalara sahip olmadıkça, varsayılanın lehine bir argüman olabilir. Ancak, Standart variadic append()optimum hem olurdu ve okunabilir ...
underscore_d

Yanıtlar:


86

Gerçekten verimliliğe ihtiyacınız olmadıkça fazladan çalışmaya muhtemelen değmez. Bunun yerine + = operatörünü kullanarak muhtemelen çok daha iyi bir verim elde edeceksiniz.

Şimdi bu feragatnameden sonra, asıl sorunuzu cevaplayacağım ...

STL dizgi sınıfının verimliliği, kullandığınız STL'nin uygulanmasına bağlıdır.

Yerleşik c işlevler aracılığıyla manuel olarak birleştirme yaparak verimliliği garanti edebilir ve daha fazla kontrole sahip olabilirsiniz .

Operatör + neden verimli değil:

Şu arayüze bir göz atın:

template <class charT, class traits, class Alloc>
basic_string<charT, traits, Alloc>
operator+(const basic_string<charT, traits, Alloc>& s1,
          const basic_string<charT, traits, Alloc>& s2)

Her + 'dan sonra yeni bir nesnenin döndürüldüğünü görebilirsiniz. Bu, her seferinde yeni bir arabellek kullanıldığı anlamına gelir. Bir ton ekstra + işlem yapıyorsanız bu verimli değildir.

Neden daha verimli hale getirebilirsiniz:

  • Bir temsilcinin bunu sizin için verimli bir şekilde yapacağına güvenmek yerine verimliliği garanti ediyorsunuz
  • std :: string sınıfı, dizginizin maksimum boyutu hakkında hiçbir şey bilmiyor, ne sıklıkta dizgenizi birleştiriyor olacaksınız. Bu bilgiye sahip olabilirsiniz ve bu bilgilere dayanarak bir şeyler yapabilirsiniz. Bu, daha az yeniden tahsis yapılmasına yol açacaktır.
  • Arabellekleri manuel olarak kontrol edeceksiniz, böylece bunun olmasını istemediğinizde tüm dizeyi yeni arabelleklere kopyalamayacağınızdan emin olabilirsiniz.
  • Yığını, çok daha verimli olan yığın yerine tamponlarınız için kullanabilirsiniz.
  • string + operatörü yeni bir string nesnesi yaratacak ve onu yeni bir tampon kullanarak geri döndürecektir.

Uygulama için dikkat edilmesi gerekenler:

  • İp uzunluğunu takip edin.
  • Bir göstericiyi dizenin sonunda ve başlangıcında veya sadece başlangıcında tutun ve dizenin sonunu bulmak için bir uzaklık olarak başlangıç ​​+ uzunluğu kullanın.
  • Dizenizi sakladığınız arabelleğin yeterince büyük olduğundan emin olun, böylece verileri yeniden ayırmanıza gerek kalmaz.
  • Dizenin sonunu bulmak için dizenin uzunluğu boyunca yineleme yapmanıza gerek kalmaması için strcat yerine strcpy kullanın.

Halat veri yapısı:

Gerçekten hızlı birleştirme işlemlerine ihtiyacınız varsa, bir halat veri yapısı kullanmayı düşünün .


6
Not: "STL", bir kısmı ISO Standart C ++ Kitaplığı'nın bazı bölümleri için temel olarak kullanılan, orijinal olarak HP tarafından tamamen ayrı bir açık kaynak kitaplığı anlamına gelir. Bununla birlikte, "std :: string" hiçbir zaman HP'nin STL'sinin bir parçası olmadı, bu nedenle "STL ve" string "i birlikte referans almak tamamen yanlış.
James Curran 04

1
STL ve dizeyi birlikte kullanmanın yanlış olduğunu söylemem. Bkz. Sgi.com/tech/stl/table_of_contents.html
Brian R. Bondy

1
SGI, STL'nin bakımını HP'den devraldığında, Standart Kitaplığa uyacak şekilde yeniden takıldı (bu yüzden "HP'nin STL'sinin asla bir parçası değil" dedim). Bununla birlikte, std :: string'in yaratıcısı ISO C ++ Komitesidir.
James Curran 04

2
Yan not: Uzun yıllar STL'nin korunmasından sorumlu olan SGI çalışanı, aynı zamanda ISO C ++ Standardizasyon Komitesi'nin Kütüphane alt grubuna da başkanlık eden Matt Austern'di.
James Curran 04

4
Yığını neden çok daha verimli olan yığın yerine tamponlarınız için kullanabileceğinizi açıklığa kavuşturabilir veya biraz puan verebilir misiniz ? ? Bu verimlilik farkı nereden geliyor?
h7r

76

Daha önce son alanınızı ayırın, ardından ekleme yöntemini bir tamponla kullanın. Örneğin, son dize uzunluğunuzun 1 milyon karakter olmasını beklediğinizi varsayalım:

std::string s;
s.reserve(1000000);

while (whatever)
{
  s.append(buf,len);
}

17

Bununla endişelenmek istemem. Bunu bir döngü içinde yaparsanız, dizeler yeniden tahsisleri en aza indirmek için her zaman belleği önceden tahsis eder - sadece operator+=bu durumda kullanın . Ve manuel olarak yaparsanız, bunun gibi veya daha uzun

a + " : " + c

Sonra, derleyici bazı dönüş değeri kopyalarını eleyebilse bile geçiciler yaratıyor. Bunun nedeni, art arda çağrılmasında operator+, referans parametresinin adlandırılmış bir nesneye mi yoksa bir alt çağırmadan geçici olarak döndürülen bir nesneye mi başvurduğunu bilmemesidir operator+. İlk önce profil oluşturmadan endişelenmemeyi tercih ederim. Ama bunu göstermek için bir örnek alalım. Bağlamayı açıklığa kavuşturmak için önce parantez kullanıyoruz. Açıklık için kullanılan işlev bildiriminin hemen arkasına argümanları koyuyorum. Bunun altında, ortaya çıkan ifadenin ne olduğunu gösteriyorum:

((a + " : ") + c) 
calls string operator+(string const&, char const*)(a, " : ")
  => (tmp1 + c)

Şimdi, bu ek olarak, tmp1gösterilen argümanlarla + operatörüne yapılan ilk çağrı tarafından döndürülen şeydir. Derleyicinin gerçekten akıllı olduğunu ve dönüş değeri kopyasını optimize ettiğini varsayıyoruz. Bu yüzden concatenation içeren tek bir yeni dize ile bitirmek ave " : ". Şimdi, bu olur:

(tmp1 + c)
calls string operator+(string const&, string const&)(tmp1, c)
  => tmp2 == <end result>

Bunu şununla karşılaştırın:

std::string f = "hello";
(f + c)
calls string operator+(string const&, string const&)(f, c)
  => tmp1 == <end result>

Aynı işlevi geçici ve adlandırılmış bir dizge için kullanıyor! Yani derleyici sahip olduğu için yeni bir dizeye ve ekleme yapılması halinde argüman kopyalayıp gövdesinden iade etmek operator+. Bir geçicinin anısını alıp ona ekleyemez. İfade ne kadar büyükse, dizelerin o kadar çok kopyasının yapılması gerekir.

Sonraki Visual Studio ve GCC, c ++ 1x'in taşıma semantiğini ( kopya semantiğini tamamlayan ) ve deneysel bir ekleme olarak rvalue referanslarını destekleyecektir. Bu, parametrenin geçici olup olmadığını anlamaya izin verir. Bu, bu tür eklemeleri inanılmaz derecede hızlı hale getirecektir, çünkü yukarıdakilerin tümü kopyasız bir "eklenti hattı" ile sonuçlanacaktır.

Bir darboğaz olduğu ortaya çıkarsa, yine de yapabilirsiniz

 std::string(a).append(" : ").append(c) ...

appendÇağrılar için argüman eklemek *thisve daha sonra kendilerine bir başvuru döndürür. Yani orada geçicilerin kopyalanması yapılmaz. Veya alternatif operator+=olarak kullanılabilir, ancak önceliği düzeltmek için çirkin parantezlere ihtiyacınız olacaktır.


Stdlib uygulayıcılarının gerçekten bunu yaptığını kontrol etmem gerekiyordu. : P libstdc++için operator+(string const& lhs, string&& rhs)yapar return std::move(rhs.insert(0, lhs)). Sonra iki eğer geçicileri vardır onun operator+(string&& lhs, string&& rhs)eğer lhsyeterli kapasite iradeye sahip sadece doğrudan append(). Bu riskler daha yavaş olmak düşünmek nerede operator+=olursa olduğunu lhs, yeterli kapasiteye sahip olmadığından o zaman geri düşer rhs.insert(0, lhs)tampon uzatmak & gibi yeni içerikleri eklemelisiniz sadece hangi append()değil, aynı zamanda orijinal içeriğinin boyunca kayması gerekiyor rhshakkı.
underscore_d

Kıyasla genel diğer parçası operator+=olmasıdır operator+için sahip olduğu, bu yüzden de bir değer dönmelidir move()hangisi daha eklenir işlem gören. Yine de, tüm dizeyi derin kopyalamaya kıyasla oldukça küçük bir ek yük (birkaç işaretçi / boyut kopyalamak), yani iyi!
underscore_d

11

Çoğu uygulama için önemli olmayacak. Sadece + operatörünün tam olarak nasıl çalıştığını bilmeden kodunuzu yazın ve yalnızca belirgin bir darboğaz haline gelirse sorunları kendi ellerinize alın.


7
Elbette çoğu durumda buna değmez ama bu onun sorusuna gerçekten cevap vermiyor.
Brian R. Bondy 04

1
Evet. "Profil sonra optimize et" demenin soruya yorum olarak eklenebileceğini kabul ediyorum :)
Johannes Schaub - litb

6
Teknik olarak, bunların "gerekli" olup olmadığını sordu. Değiller ve bu soruyu yanıtlıyor.
Samantha Branham 04

Yeterince adil, ancak bazı uygulamalar için kesinlikle gereklidir. Dolayısıyla, bu uygulamalarda cevap şu şekildedir: 'meseleleri kendi elinize alın'
Brian R. Bondy 04

4
@Pesto Programlama dünyasında performansın önemli olmadığı konusunda sapkın bir düşünce var ve biz tüm anlaşmayı görmezden gelebiliriz çünkü bilgisayarlar gittikçe daha hızlı hale geliyor. Mesele şu ki, insanların C ++ programlamasının nedeni bu değil ve verimli dizgi birleştirme hakkında yığın taşması üzerine sorular göndermelerinin nedeni bu değil.
MrFox

7

.NET System.Strings aksine, C ++ 'ın std :: dizeleri olan değişken ve bu nedenle basit dizilim yoluyla kadar hızlı gibi diğer yöntemlerle inşa edilebilir.


2
Özellikle, başlamadan önce sonuç için arabelleği yeterince büyük yapmak için rezerv () kullanırsanız.
Mark Ransom

bence operatör + = hakkında konuşuyor. dejenere bir durum olmasına rağmen, aynı zamanda bir araya geliyor. james bir vc ++ mvp idi, bu yüzden c ++: p
Johannes Schaub - litb

1
C ++ konusunda kapsamlı bilgi birikimine sahip olduğundan bir an bile şüphem yok, sadece soru hakkında bir yanlış anlaşılma vardı. Soru, her çağrıldığında yeni dize nesneleri döndüren ve dolayısıyla yeni karakter arabellekleri kullanan + operatörünün verimliliği hakkında soruldu.
Brian R. Bondy 04

1
Evet. ama sonra durum operatörü + yavaş, en iyi yol birleştirme yapmanın ne olduğunu sordu. ve burada operatör + = oyuna giriyor. ama james'in cevabının biraz kısa olduğuna katılıyorum. hepimizin operatör + kullanabileceği gibi görünmesini sağlıyor ve en verimli: p
Johannes Schaub - litb

@ BrianR.Bondy'nin operator+yeni bir dizge döndürmesi gerekmez. Uygulayıcılar, işlenen rvalue referansı ile aktarılmışsa, işlenenlerinden birini değiştirilmiş olarak döndürebilir. libstdc++ örneğin bunu yapar . Bu nedenle, geçicilerle arama yaparken operator+, aynı veya neredeyse aynı performansa ulaşabilir - bu, bir darboğazı temsil ettiğini gösteren kıyaslamalara sahip olmadıkça, temerrüde düşme lehine başka bir argüman olabilir.
underscore_d


4

In Kusurlu C ++ , Matthew Wilson sunar dinamik tüm parçaları birleştirerek önce tek tahsisi sahip olmak için son dize uzunluğunu önceden hesaplar olduğunu dize Concatenator. İfade şablonlarıyla oynayarak da statik bir birleştirici uygulayabiliriz .

Bu tür bir fikir STLport std :: string uygulamasında uygulandı - bu kesin hack nedeniyle standarda uymuyor.


Glib::ustring::compose()glibmm bağlamalarından GLib'e şunu yapar: reserve()sağlanan biçim dizgisine ve değişkenlere dayalı olarak son uzunluğu tahmin eder ve s, sonra append()her biri (veya biçimlendirilmiş ikamesi) bir döngüde olur. Bunun oldukça yaygın bir çalışma şekli olduğunu umuyorum.
underscore_d

4

std::string operator+yeni bir dizge ayırır ve her seferinde iki işlenen dizgiyi kopyalar. birçok kez tekrarlayın ve pahalı hale gelir, O (n).

std::string appendve operator+=diğer yandan, dizinin her büyümesi gerektiğinde kapasiteyi% 50 arttırın. Bellek ayırma ve kopyalama işlemlerinin sayısını önemli ölçüde azaltan, O (log n).


Bunun neden reddedildiğinden pek emin değilim. % 50 rakamı Standart tarafından zorunlu tutulmamaktadır, ancak IIRC veya% 100 pratikte yaygın büyüme ölçütleridir. Bu cevaptaki diğer her şey itiraz edilemez görünüyor.
underscore_d

Aylar sonra, C ++ 11'in piyasaya sürülmesinden çok sonra yazıldığı ve operator+bir veya iki bağımsız değişkenin rvalue başvurusu tarafından geçirildiği aşırı yükler , mevcut arabelleğe birleştirerek yeni bir dizeyi tamamen ayırmayı önleyebilir. işlenenlerden biri (yetersiz kapasiteye sahipse yeniden tahsis etmek zorunda olsalar da).
underscore_d

2

Küçük dizeler için önemli değil. Büyük dizeleriniz varsa, onları vektörde oldukları gibi veya başka bir koleksiyonda parça olarak saklamanız daha iyi olur. Ve algoritmanızı tek bir büyük dizgi yerine böyle bir veri kümesiyle çalışacak şekilde ekleyin.

Karmaşık birleştirme için std :: ostringstream'i tercih ederim.


2

Çoğu şeyde olduğu gibi, bir şeyi yapmamak, yapmaktan daha kolaydır.

Bir GUI'ye büyük dizeler çıkarmak istiyorsanız, çıktısını aldığınız her şey, dizeleri büyük bir dizeden daha iyi işleyebilir (örneğin, bir metin düzenleyicide metni birleştirme - genellikle satırları ayrı tutarlar) yapılar).

Bir dosyaya çıktı vermek istiyorsanız, büyük bir dize oluşturmak ve bunu çıkarmak yerine verileri akışa alın.

Yavaş koddan gereksiz birleştirmeyi kaldırırsam, daha hızlı birleştirme ihtiyacını asla bulamadım.


2

Ortaya çıkan dizede alanı önceden ayırırsanız (ayırırsanız) muhtemelen en iyi performans.

template<typename... Args>
std::string concat(Args const&... args)
{
    size_t len = 0;
    for (auto s : {args...})  len += strlen(s);

    std::string result;
    result.reserve(len);    // <--- preallocate result
    for (auto s : {args...})  result += s;
    return result;
}

Kullanım:

std::string merged = concat("This ", "is ", "a ", "test!");

0

Dizi boyutunu ve ayrılan bayt sayısını izleyen bir sınıfta kapsüllenmiş basit bir karakter dizisi en hızlısıdır.

İşin püf noktası, başlangıçta yalnızca bir büyük tahsisat yapmaktır.

-de

https://github.com/pedro-vicente/table-string

Kıyaslamalar

Visual Studio 2015 için, x86 hata ayıklama derlemesi, C ++ std :: string üzerinde önemli iyileştirme.

| API                   | Seconds           
| ----------------------|----| 
| SDS                   | 19 |  
| std::string           | 11 |  
| std::string (reserve) | 9  |  
| table_str_t           | 1  |  

1
OP, nasıl verimli bir şekilde birleştirileceğiyle ilgilenir std::string. Alternatif bir string sınıfı istemiyorlar.
underscore_d

0

Bunu her öğe için bellek ayırmalarıyla deneyebilirsiniz:

namespace {
template<class C>
constexpr auto size(const C& c) -> decltype(c.size()) {
  return static_cast<std::size_t>(c.size());
}

constexpr std::size_t size(const char* string) {
  std::size_t size = 0;
  while (*(string + size) != '\0') {
    ++size;
  }
  return size;
}

template<class T, std::size_t N>
constexpr std::size_t size(const T (&)[N]) noexcept {
  return N;
}
}

template<typename... Args>
std::string concatStrings(Args&&... args) {
  auto s = (size(args) + ...);
  std::string result;
  result.reserve(s);
  return (result.append(std::forward<Args>(args)), ...);
}
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.