Issue Details (XML | Word | Printable)

Key: STDCXX-231
Type: Improvement Improvement
Status: Closed Closed
Resolution: Fixed
Priority: Major Major
Assignee: Travis Vitek
Reporter: Martin Sebor
Votes: 0
Watchers: 0
Operations

If you were logged in you would be able to see more operations.
C++ Standard Library

std::getline from <string> header is rather slow

Created: 29/Jun/06 01:39 AM   Updated: 04/Feb/08 10:42 PM
Return to search
Component/s: 21. Strings
Affects Version/s: 4.1.2, 4.1.3, 4.1.4, 4.2.0
Fix Version/s: 4.2.1

Time Tracking:
Original Estimate: 4h
Original Estimate - 4h
Remaining Estimate: 0.5h
Time Spent - 7.5h Remaining Estimate - 0.5h
Time Spent: 7.5h
Time Spent - 7.5h Remaining Estimate - 0.5h

File Attachments:
  Size
Text File Licensed for inclusion in ASF works stdcxx-231.patch 2008-01-30 01:49 AM Travis Vitek 1 kB

Severity: Inefficiency
Resolution Date: 04/Feb/08 10:42 PM


 Description  « Hide
Moved from the Rogue Wave bug tracking database:
****Created By: leroy @ Jan 25, 2001 03:20:01 PM****


Environment
  Compiler : SUNPRO 4.2
  OS : Solaris 2.5.1
  SCL : 1.3.0 (Summer-1999)
  Tools : 7.1.0 (Summer-1999) --> Use only for RWBench

Command line option :
  for debug : 
  CC -xildoff +w +p -g -o Test_dbg.exe test.cc -DRWDEBUG=1 \
-I/opt/RogueWave/Summer-1999/workspaces/SOLARIS251/SUNPRO42/11s \
-I/opt/RogueWave/Summer-1999/workspaces/SOLARIS251/SUNPRO42/11s/include \
-I. -L/opt/RogueWave/Summer-1999/workspaces/SOLARIS251/SUNPRO42/11s/lib -Bstatic -ltls11s -lstd11s -Bdynamic

  for release :
CC -xildoff +w +p -fast -o Test_release.exe test.cc \
-I/opt/RogueWave/Summer-1999/workspaces/SOLARIS251/SUNPRO42/8s \
-I/opt/RogueWave/Summer-1999/workspaces/SOLARIS251/SUNPRO42/8s/include \
-I. -L/opt/RogueWave/Summer-1999/workspaces/SOLARIS251/SUNPRO42/8s/lib -Bstatic -ltls8s -lstd8s -Bdynamic
 (Uploaded file: 997149-test.cc)
                                                                     
**** Entered By: Web @ Thursday, January 25, 2001 2:41:42 AM **** 
Location of uploaded file: 
http://thoth.bco.roguewave.com/uploads/997149-test.cc

View all uploaded files for this incident: 
http://webdev.roguewave.com/admin/tsvw/index.cfm?IncidentID=997149

**** Entered By: Web @ Thursday, January 25, 2001 2:44:56 AM **** 
#web
Please find my test case at the end of the note

Environment
  Compiler : SUNPRO 4.2
  OS : Solaris 2.5.1
  SCL : 1.3.0 (Summer-1999)
  Tools : 7.1.0 (Summer-1999) --> Use only for RWBench

Command line option :
  for debug : 
  CC -xildoff +w +p -g -o Test_dbg.exe test.cc -DRWDEBUG=1 \
-I/opt/RogueWave/Summer-1999/workspaces/SOLARIS251/SUNPRO42/11s \
-I/opt/RogueWave/Summer-1999/workspaces/SOLARIS251/SUNPRO42/11s/include \
-I. -L/opt/RogueWave/Summer-1999/workspaces/SOLARIS251/SUNPRO42/11s/lib -Bstatic -ltls11s -lstd11s -Bdynamic

  for release :
CC -xildoff +w +p -fast -o Test_release.exe test.cc \
-I/opt/RogueWave/Summer-1999/workspaces/SOLARIS251/SUNPRO42/8s 
-I/opt/RogueWave/Summer-1999/workspaces/SOLARIS251/SUNPRO42/8s/include \
-I. -L/opt/RogueWave/Summer-1999/workspaces/SOLARIS251/SUNPRO42/8s/lib -Bstatic -ltls8s -lstd8s -Bdynamic

#Code
#include <string>
#include <fstream.h>
#include <iostream.h>
#include <rw/bench.h>
 
 
class std_string_getline : public RWBench
{
  public:
   std_string_getline() {;}
 
   void doLoop(unsigned long n);
   void idleLoop(unsigned long n);
   void what(ostream& os) const { os << "Standard String Getline : " << endl;}
};
 
class classic_getline : public RWBench
{
  public:
    classic_getline() {;}
 
    void doLoop(unsigned long n);
    void idleLoop(unsigned long n);
    void what(ostream& os) const { os << "Classic Getline : " << endl;}
};
 
int
main(int argc, char** argv)
{
 
  std_string_getline test_std_string;
  test_std_string.parse(argc, argv);
  test_std_string.go();
  test_std_string.report(cout);
 
  classic_getline test_classic_getline;
  test_classic_getline.parse(argc, argv);
  test_classic_getline.go();
  test_classic_getline.report(cout);
}
 
void
std_string_getline::doLoop(unsigned long n)
{
  while (n--)
  {
    ifstream toRead(__FILE__);
    string line;
    line.reserve(512);
 
    while (!(toRead.eof()))
    {
      getline(toRead, line, '\n');
    }
  }
}
 
void
std_string_getline::idleLoop(unsigned long n)
{
  while (n--)
  {
    ifstream toRead(__FILE__);
    string line;
    line.reserve(512);
  }
}
 
void
classic_getline::doLoop(unsigned long n)
{
  while (n--)
  {
    ifstream toRead(__FILE__);
    char cLine[512];
    string line;
    line.reserve(512);
 
    while (!(toRead.eof()))
    {
      toRead.getline(cLine, 512);
      line = cLine;
    }
  }
}
 
void
classic_getline::idleLoop(unsigned long n)
{
  while (n--)
  {
    ifstream toRead(__FILE__);
    char cLine[512];
    string line;
    line.reserve(512);
  }
}

#EndCode                

There appears to be something to this.  I ran the program and here is the output:

Sun C++ 

Standard String Getline : 

Iterations:                 1
Inner loop operations:      1000
Total operations:           1000
Elapsed (user) time:        18.18
Operations per second:      55.0055

Sun C++ 

Classic Getline : 

Iterations:                 5
Inner loop operations:      1000
Total operations:           5000
Elapsed (user) time:        4.67
Kilo-operations per second: 1.07066


 All   Comments   Work Log   Change History   Subversion Commits      Sort Order: Ascending order - Click to sort in descending order
Mark Brown added a comment - 23/Oct/07 05:36 PM
I just benchmarked getline() from stdcxx 4.1.3 against the same function in gcc 4.1 (the program is below), and out of curiosity against the wc utility on Linux. stdcxx is more than twice slower than gcc, and nearly ten times slower than wc.The timings for a 32MB text file are:

$ ls -l input.txt && time LD_LIBRARY_PATH=../lib ./getline 1 input.txt && time ./a.out 1 input.txt && time wc -l input.txt && cat getline.cpp
rw-rw-r- 1 mbrown mbrown 32942720 Oct 23 11:30 input.txt
892370

real 0m0.433s
user 0m0.400s
sys 0m0.036s
892370

real 0m0.149s
user 0m0.124s
sys 0m0.024s
892370 input.txt

real 0m0.053s
user 0m0.048s
sys 0m0.004s
#include <stdio.h>
#include <stdlib.h>
#include <fstream>
#include <string>

int main (int argc, char *argv[])
{
unsigned long loops;
const char* file;

loops = 1 < argc ? strtoul (argv [1], 0, 0) : 1;
file = 2 < argc ? argv [2] : _FILE_;

unsigned long line_count = 0;
std::string line;

for (unsigned long i = 0; i != loops; ++i) { std::ifstream in (file); if (!in.is_open ()) return 1; while (std::getline (in, line)) ++line_count; }

printf ("%lu\n", line_count);
}


Martin Sebor added a comment - 10/Dec/07 07:06 AM
Affects all released versions.
Let's try to look into speeding this up in 4.2.1.

Travis Vitek added a comment - 30/Jan/08 01:04 AM - edited
All of the above testcases are testing both the time taken by getline() and the time to create and setup a file stream. The following testcase tests just the time it takes to do the getline, everything else should be constant.
#include <iostream>
#include <fstream>
#include <string>

#include <stdlib.h>

int main (int argc, char* argv[])
{
    size_t loops = 1000;
    if (1 < argc)
        loops = strtoul (argv [1], 0, 0);

    const char* file = __FILE__;
    if (2 < argc)
        file = argv [2];

    std::ifstream ifs (file);
    if (!ifs.is_open ())
        return 1;

    size_t lines = 0;

    std::string line;
    for (size_t i = 0; i < loops; ++i)
    {
        while (std::getline (ifs, line, '\n'))
            ++lines;

        ifs.clear ();
        ifs.seekg (0);
    }

    std::cout << lines << std::endl;
    return 0;
}

Travis Vitek added a comment - 30/Jan/08 01:49 AM
The issue is that the std::basic_string<...>::erase() method deallocates the string body, and then immediately reallocates a new one when we call append(). Obviously this is an unnecessary cost, and it can be quite expensive.

The std::basic_string<...>::clear(), erase() and replace() methods all have this behavior when resizing down to a zero length string. I'm inclined to believe that this is a simple mistake, but it is possible that it was done intentionally so that strings are deallocated when they become empty. My intention is to modify clear() and replace() to only deallocate the body when actually necessary. This will have the side effect that a string that has its size reduced to 0 will continue to hold the memory that it used prior to that resize. If a user wishes to deallocate this memory, they will have to use the following..

  // swap implementations with a null string to deallocate memory
  std::string ().swap (s);

Another option is to leave the current behavior for erase() and replace(), and to fix the issue for clear() [or vice-versa].

Here are the results of running the test before any changes...

$ time ./t-gcc 1000 /nfs/devco/vitek/stdcxx/trunk/tests/strings/21.string.io.cpp 
1291000

real    0m0.587s
user    0m0.563s
sys     0m0.021s
$ time ./t-stdcxx 1000 /nfs/devco/vitek/stdcxx/trunk/tests/strings/21.string.io.cpp
1291000

real    0m1.022s
user    0m0.907s
sys     0m0.068s

Here is the result after the patch...

$ time ./t-stdcxx 1000 /nfs/devco/vitek/stdcxx/trunk/tests/strings/21.string.io.cpp
1291000

real    0m0.309s
user    0m0.226s
sys     0m0.081s

Here is the changelog

2008-01-29  Travis Vitek  <vitek@roguewave.com>

	STDCXX-231
	* include/string (clear): Avoid deallocating string body unless
	necessary.
	* include/string.cc (replace): Ditto.
	* include/istream.cc (getline): Call clear() instead of erase()
	to avoid unnecessary overhead.

Martin Sebor added a comment - 30/Jan/08 02:40 AM
Prevented the Wiki plugin from formatting the Description.

Martin Sebor added a comment - 30/Jan/08 03:05 AM
Wow, that's quite an improvement! IMO, not deallocating string memory on clear() or erase() makes perfect sense. I'm just not completely sure that it isn't too big a change for a micro release. If there is code out there that's optimized for space and that assumes these functions have deallocating semantics we might be changing its performance characteristics in an unexpected way. I might feel more comfortable if I knew that most other implementations did deallocate... Then again, maybe I'm being overly cautious...

Travis Vitek added a comment - 30/Jan/08 08:42 AM
This is the Dinkum implemenation on MSVC 8.0...
C:\>type t.cpp && cl /nologo /EHsc t.cpp && t.exe
#include <string>
#include <iostream>

int main ()
{
    std::string s (1000, 'a');
    std::cout << s.capacity () << std::endl;

    s.clear ();
    std::cout << s.capacity () << std::endl;

    s.erase ();
    std::cout << s.capacity () << std::endl;

    s.replace (0, s.size (), "1", 0);
    std::cout << s.capacity () << std::endl;

    std::string ().swap (s);
    std::cout << s.capacity () << std::endl;

    return 0;
}
t.cpp
1007
1007
1007
1007
15

And this is the result with the GNU implementation...

$ g++ t.cpp && a.out
1095
1095
1095
1095
0

Travis Vitek added a comment - 30/Jan/08 08:46 AM
Fix format so that page doesn't run over.

Martin Sebor added a comment - 30/Jan/08 11:51 PM
Thanks for the numbers! I'm guessing the capacity doesn't drop below 15 in the Dinkumware string because they implement the small string optimization.

So as far as fixing this in 4.2.1 or 4.3, what do you propose?


Travis Vitek added a comment - 31/Jan/08 09:20 PM - edited
Well, this fix is functionally incompatible. According to our compatibility guidelines, this makes it binary incompatible, so it would not be suitable for 4.2.1. Personally, I'd like to commit it for 4.2.1, but that would be against the rules.

So for now it looks like the right thing to do is to submit it to trunk and just not merge it out to 4.2.x. Committed to trunk in r617251.


Travis Vitek added a comment - 31/Jan/08 11:00 PM
Upon further discussion, Martin and I have decided that the proposed change is acceptable for 4.2.1. It is a small change in behavior, but it makes our implementation consistent with others and it improves performance significantly.

Regression test added in r617276
Merged to 4.2.x in r617280


Travis Vitek added a comment - 04/Feb/08 10:42 PM
Verified in nightly testing.