簡體   English   中英

與以null終止的字符串相比,std :: string的效率如何?

[英]How efficient is std::string compared to null-terminated strings?

我發現std::string s與老式的以null終止的字符串相比非常慢,它是如此之慢,以至於它們將我的整個程序的速度降低了2倍。

我以為STL會慢一些,但我沒有意識到它會這么慢。

我正在使用Visual Studio 2008發行模式。 它顯示字符串的分配要比char*分配慢100-1000倍(測試char*分配的運行時非常困難)。 我知道這不是一個公平的比較,指針分配與字符串復制比較,但是我的程序有很多字符串分配,而且我不確定是否可以在所有地方使用“ const reference ”技巧。 通過引用計數實現,我的程序會很好,但是這些實現似乎不再存在。

我真正的問題是:為什么人們現在不再使用引用計數實現,這是否意味着我們所有人都需要更加謹慎地避免std :: string的常見性能陷阱?

我的完整代碼如下。

#include <string>
#include <iostream>
#include <time.h>

using std::cout;

void stop()
{
}

int main(int argc, char* argv[])
{
    #define LIMIT 100000000
    clock_t start;
    std::string foo1 = "Hello there buddy";
    std::string foo2 = "Hello there buddy, yeah you too";
    std::string f;
    start = clock();
    for (int i=0; i < LIMIT; i++) {
        stop();
        f = foo1;
        foo1 = foo2;
        foo2 = f;
    }
    double stl = double(clock() - start) / CLOCKS\_PER\_SEC;

    start = clock();
    for (int i=0; i < LIMIT; i++) {
        stop();
    }
    double emptyLoop = double(clock() - start) / CLOCKS_PER_SEC;

    char* goo1 = "Hello there buddy";
    char* goo2 = "Hello there buddy, yeah you too";
    char *g;
    start = clock();
    for (int i=0; i < LIMIT; i++) {
        stop();
        g = goo1;
        goo1 = goo2;
        goo2 = g;
    }
    double charLoop = double(clock() - start) / CLOCKS_PER_SEC;
    cout << "Empty loop = " << emptyLoop << "\n";
    cout << "char* loop = " << charLoop << "\n";
    cout << "std::string = " << stl << "\n";
    cout << "slowdown = " << (stl - emptyLoop) / (charLoop - emptyLoop) << "\n";
    std::string wait;
    std::cin >> wait;
    return 0;
}

關於字符串和其他容器的性能,肯定存在一些已知問題。 其中大多數與臨時文件和不必要的副本有關。

正確使用它並不難,但是做錯它也很容易。 例如,如果您在不需要可修改參數的地方看到代碼按值接受字符串,則您做錯了:

// you do it wrong
void setMember(string a) {
    this->a = a; // better: swap(this->a, a);
}

您最好通過const引用或在內部執行交換操作(而不是另一個副本)來執行此操作。 在這種情況下,向量或列表的性能損失會增加。 但是,您肯定是存在已知問題的。 例如:

// let's add a Foo into the vector
v.push_back(Foo(a, b));

我們正在創建一個臨時Foo只是將一個新的Foo添加到我們的向量中。 在手動解決方案中,這可能直接將Foo創建到向量中。 而且,如果向量達到其容量極限,則必須為其元素重新分配更大的內存緩沖區。 它有什么作用? 它將使用其復制構造函數將每個元素分別復制到其新位置。 如果手動解決方案事先知道元素的類型,則可能會表現得更聰明。

另一個常見問題是臨時性問題。 看看這個

string a = b + c + e;

創建了許多臨時工,您可以在實際優化性能的自定義解決方案中避免這些臨時工。 那時, std::string的接口被設計為寫時復制友好的。 但是,隨着線程變得越來越流行,寫字符串上的透明副本在保持其狀態一致方面存在問題。 最近的實現傾向於避免在寫字符串上進行復制,而是在適當的地方應用其他技巧。

但是,大多數問題將在下一版標准中解決。 例如,您可以使用emplace_back代替push_back直接在向量中創建Foo

v.emplace_back(a, b);

並且,除了在上面的級聯中創建副本之外, std::string會識別何時將臨時級聯並針對這些情況進行優化。 重新分配也將避免制作副本,但會將元素移動到適合其新位置的位置。

要獲得出色的閱讀,請考慮Andrei Alexandrescu的Move Constructors

但是,有時比較也往往是不公平的。 標准容器必須支持其必須支持的功能。 例如,如果您的容器在添加/刪除地圖元素時沒有使地圖元素引用保持有效,則將“更快”的地圖與標准地圖進行比較可能會變得不公平,因為標准地圖必須確保元素保持有效。 當然,這只是一個例子,在聲明“我的容器比標准容器快!!”時,有許多情況需要牢記。

好像您在粘貼的代碼中誤用了char *。 如果你有

std::string a = "this is a";
std::string b = "this is b"
a = b;

您正在執行字符串復制操作。 如果對char *執行相同的操作,則將執行指針復制操作。

std :: string賦值操作分配足夠的內存以將b的內容保存在a中,然后一個一個地復制每個字符。 對於char *,它不進行任何內存分配,也不逐個復制單個字符,只是說“ a現在指向b所指向的相同內存”。

我的猜測是這就是為什么std :: string變慢的原因,因為它實際上是在復制字符串,這似乎是您想要的。 要對char *進行復制操作,您需要使用strcpy()函數復制到已經適當大小的緩沖區中。 然后,您將進行准確的比較。 但是出於程序目的,您幾乎應該絕對使用std :: string代替。

使用任何實用工具類(無論是STL還是您自己的)而不是例如編寫C ++代碼時。 好的舊C空終止字符串,您需要記住一些事情。

  • 如果在沒有編譯器優化的情況下進行基准測試(尤其是函數內聯),則類將丟失。 它們不是內置的,甚至不是stl。 它們是根據方法調用實現的。

  • 不要創建不必要的對象。

  • 如果可能,請勿復制對象。

  • 傳遞對象作為參考,而不是副本,如果可能的話,

  • 使用更專業的方法和功能以及更高級別的算法。 例如。:

     std::string a = "String a" std::string b = "String b" // Use a.swap(b); // Instead of std::string tmp = a; a = b; b = tmp; 

最后一點。 當類似C的C ++代碼開始變得越來越復雜時,您需要實現更高級的數據結構,例如自動擴展數組,字典,高效的優先級隊列。 突然間,您意識到它的工作量很大,並且您的課程並沒有真正比stl更快。 只是越野車。

您肯定在做錯事,或者至少沒有在STL和您自己的代碼之間進行“比較”比較。 當然,沒有代碼就很難變得更加具體。

可能是因為您正在使用STL構造代碼,從而導致更多的構造函數運行,或者沒有以與您自己實現操作時所執行的操作相匹配的方式重用分配的對象。

此測試正在測試兩個根本不同的事物:淺副本與深副本。 必須了解差異以及如何避免在C ++中進行深度復制,因為默認情況下,C ++對象為其實例提供值語義(與普通舊數據類型的情況一樣),這意味着通常會分配一個實例給另一個實例復制。

我“更正”了您的測試並得到了:

char* loop = 19.921
string = 0.375
slowdown = 0.0188244

顯然,我們應該停止使用C風格的字符串,因為它們的運行速度要慢得多! 實際上,我故意通過在字符串側測試淺拷貝而不是在strcpy上進行測試,使您的測試與您一樣有缺陷

#include <string>
#include <iostream>
#include <ctime>

using namespace std;

#define LIMIT 100000000

char* make_string(const char* src)
{
    return strcpy((char*)malloc(strlen(src)+1), src);
}

int main(int argc, char* argv[])
{
    clock_t start;
    string foo1 = "Hello there buddy";
    string foo2 = "Hello there buddy, yeah you too";
    start = clock();
    for (int i=0; i < LIMIT; i++)
        foo1.swap(foo2);
    double stl = double(clock() - start) / CLOCKS_PER_SEC;

    char* goo1 = make_string("Hello there buddy");
    char* goo2 = make_string("Hello there buddy, yeah you too");
    char *g;
    start = clock();
    for (int i=0; i < LIMIT; i++) {
        g = make_string(goo1);
        free(goo1);
        goo1 = make_string(goo2);
        free(goo2);
        goo2 = g;
    }
    double charLoop = double(clock() - start) / CLOCKS_PER_SEC;
    cout << "char* loop = " << charLoop << "\n";
    cout << "string = " << stl << "\n";
    cout << "slowdown = " << stl / charLoop << "\n";
    string wait;
    cin >> wait;
}

要點是,這實際上成為您最終問題的核心,您必須知道您在使用代碼做什么。 如果您使用的是C ++對象,則必須知道將一個對象分配給另一個對象會復制該對象(除非禁用分配,否則會出現錯誤)。 您還必須知道何時適合使用指向對象的引用,指針或智能指針,並且對於C ++ 11,還應該了解移動和復制語義之間的區別。

我真正的問題是:為什么人們現在不再使用引用計數實現,這是否意味着我們所有人都需要更加謹慎地避免std :: string的常見性能陷阱?

人們確實使用引用計數實現。 這是一個例子:

shared_ptr<string> ref_counted = make_shared<string>("test");
shared_ptr<string> shallow_copy = ref_counted; // no deep copies, just 
                                               // increase ref count

區別在於字符串在內部不執行,因為對於那些不需要字符串的人來說效率不高。 由於類似的原因(再加上通常會使線程安全成為問題的事實),通常也不再對字符串執行寫時復制之類的操作。 但是,如果我們願意的話,我們這里有所有的構建塊都可以進行寫時復制:我們能夠交換字符串而無需任何深層復制,我們能夠為其創建指針,引用或智能指針。 。

為了有效地使用C ++,您必須習慣於這種涉及值語義的思維方式。 如果不這樣做,您可能會享受到額外的安全性和便利性,但是這樣做卻會大大提高代碼的效率(不必要的副本無疑是導致編寫質量差的C ++代碼比C慢的重要原因)。 畢竟,您的原始測試仍在處理指向字符串的指針,而不是char[]數組。 如果您使用字符數組而不是指向它們的指針,則同樣需要strcpy來交換它們。 使用字符串,您甚至可以使用內置的交換方法來有效地准確執行測試中的工作,因此我的建議是花更多的時間學習C ++。

如果有跡象表明向量的最終大小,則可以在填充向量之前調用reserve()來防止大小過多。

優化的主要規則:

  • 規則1:不要這樣做。
  • 規則2 :(僅適用於專家)不要這樣做。

您確定已證明確實是STL慢,而不是算法慢嗎?

使用STL並非總是容易獲得良好的性能,但通常來說,它旨在為您提供強大的功能。 我發現Scott Meyers的“有效STL”讓您大開眼界,以了解如何有效地處理STL。 讀!

正如其他人所說,您可能會遇到字符串的頻繁深拷貝,並將其與指針分配/引用計數實現進行比較。

通常,針對您的特定需求而設計的任何類都將勝過針對一般情況設計的通用類。 但是,學習好好使用通用類並學習遵守80:20的規則,您將比別人自己滾動一切的效率更高。


std::string一個特定缺點是它不提供性能保證,這是有道理的。 正如蒂姆·庫珀(Tim Cooper)所述,STL並未說明字符串分配是否創建了深拷貝。 這對於通用類來說是很好的,因為引用計數可能會成為高度並發應用程序中的真正殺手,盡管它通常是單線程應用程序的最佳方法。

string  const string&   char*   Java string
---------------------------------------------------------------------------------------------------
Efficient               no **       yes         yes     yes
assignment                          

Thread-safe             yes         yes         yes     yes

memory management       yes         no          no      yes
done for you

** std :: string有2種實現:引用計數或深度復制。 引用計數會在多線程程序中引入性能問題,即使只是讀取字符串也是如此,而深度復制顯然較慢,如上所示。 請參閱: 為什么不對VC ++字符串進行引用計數?

如該表所示,“字符串”在某些方面優於“ char *”,而在其他方面則較差,並且“ const string&”的屬性與“ char *”相似。 我個人將在許多地方繼續使用'char *'。 無聲地復制std :: string的大量復制,使用隱式復制構造函數和臨時副本,使我對std :: string有點矛盾。

他們沒有錯。 一般來說,STL實施要比您的實施好。

我確信您可以針對特定情況編寫更好的東西,但是系數2太大了……您確實必須做錯了什么。

如果正確使用,std :: string與char *一樣有效,但是具有附加的保護。

如果您在使用STL時遇到性能問題,則可能是您做錯了什么。

此外,STL實現不是跨編譯器的標准配置。 我知道SGI的STL和STLPort通常表現良好。

話雖如此,但我很認真地說,您可能是C ++天才,並且設計的代碼比STL更復雜。 不太可能,但是誰知道,您可能是C ++的勒布朗·詹姆斯。

我想說STL實現比傳統實現更好。 您還嘗試過使用列表而不是向量嗎,因為向量對於某些目的是有效的,而列表對於其他目的是有效的

std::string 總是比C字符串慢。 C字符串只是內存的線性數組。 僅作為數據結構,您將獲得比這更高的效率。 您使用的算法(例如strcat()strcpy() )通常等效於STL對應的算法。 相對而言,類實例化和方法調用將比C字符串操作慢得多(如果實現使用虛函數,則更糟)。 獲得等效性能的唯一方法是編譯器進行優化。

原因的很大一部分可能是事實,即STL的現代實現中不再使用引用計數。

這是故事(如果我錯了,請糾正我):開始時,STL實現使用引用計數,並且速度很快,但不是線程安全的-實現者希望應用程序程序員在更高級別上插入自己的鎖定機制,以使它們是線程安全的,因為如果在2個級別上進行鎖定,那么這將使速度降低兩倍。

但是,世界上的程序員太無知或懶惰,無法在任何地方插入鎖。 例如,如果多線程程序中的工作線程需要讀取std :: string命令行參數,則即使只是讀取字符串也需要鎖定,否則可能導致崩潰。 (2個線程在不同的CPU上同時增加參考計數(+1),但分別減少(-2),因此參考計數降至零,並釋放了內存。)

因此,實現者放棄了引用計數,而是讓每個std :: string始終擁有自己的字符串副本。 更多的程序可以運行,但是速度都較慢。

所以現在,即使是將一個std :: string分配給另一個(或等效地,將std :: string作為參數傳遞給函數),也要花費大約400條機器代碼指令,而不是分配char所需的2條代碼*,減速200倍。

我在一個主要程序上測試了std :: string效率低下的程度,該程序與以null終止的字符串相比,整體速度降低了約100%。 我還使用以下代碼測試了原始std :: string分配,該代碼表示​​std :: string分配的速度慢了100-900倍。 (我無法測量char *分配的速度)。 我還調試了std :: string運算符=()函數-在擊中“ memcpy()”之前,我最終膝蓋深處進入了堆棧,大約深了7層。

我不確定是否有解決方案。 也許如果您需要程序快速運行,請使用普通的舊C ++,並且如果您更擔心自己的工作效率,則應使用Java。

#define LIMIT 800000000
clock_t start;
std::string foo1 = "Hello there buddy";
std::string foo2 = "Hello there buddy, yeah you too";
std::string f;

start = clock();
for (int i=0; i < LIMIT; i++) {
    stop();
    f    = foo1;
    foo1 = foo2;
    foo2 = f;
}
double stl = double(clock() - start) / CLOCKS_PER_SEC;

start = clock();
for (int i=0; i < LIMIT; i++) {
    stop();
}
double emptyLoop = double(clock() - start) / CLOCKS_PER_SEC;

char* goo1 = "Hello there buddy";
char* goo2 = "Hello there buddy, yeah you too";
char *g;

start = clock();
for (int i=0; i < LIMIT; i++) {
    stop();
    g = goo1;
    goo1 = goo2;
    goo2 = g;
}
double charLoop = double(clock() - start) / CLOCKS_PER_SEC;

TfcMessage("done", 'i', "Empty loop = %1.3f s\n"
                        "char* loop = %1.3f s\n"
                        "std::string loop = %1.3f s\n\n"
                        "slowdown = %f", 
                        emptyLoop, charLoop, stl, 
                        (stl - emptyLoop) / (charLoop - emptyLoop));

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM