c-内存/速度问题的一般策略

我有一个C代码,可以运行大约200个ASCII文件,进行一些基本的数据处理,并输出一个(基本上)所有数据的ASCII文件.

该程序起初运行非常快,然后在整个过程中急剧减慢,也许会逐渐减慢一点,然后在其余过程中以相当慢的速度进行.即它会在大约5秒钟内读取大约80个文件,在大约50秒钟内读取大约200个文件.每个文件基本相同.

我正在寻找有关如何解决问题或内存泄漏的建议.

一些更多的细节:
首先,我将在程序开始时打开fopen(FILE * outputFile,“ w”),并在结束时打开fclose().前40个文件大约需要4秒钟;然后大约1.5分钟即可获取约200个文件.

我以为输出文件可能会阻塞内存,所以我每次迭代(即每次打开新文件时)都将代码更改为fopen(outputFile,“ a”),并且每次关闭输入文件时都将代码更改为fclose().如上所述,这将性能提高到总计约50秒.

似乎奇怪的是,此“修复”会如此明显地帮助您,但并非完全如此.

另外,我也不动态分配任何内存(没有对“ new”或“ delete”或“ free”之类的调用)……所以我什至不知道如何发生内存泄漏.

任何帮助,将不胜感激!
谢谢!

码:

vector<string> dirCon;
// Uses boost::filesystem to store every file in directory
bool retVal = FileSystem::getDirectoryContents(HOME_DIR+HISTORY_DIR, &dirCon, 2);

int counter = 0;
for(int i = 0; i < dirCon.size(); i++) { 
    // Create output file
    FILE *outFile;
    string outputFileName = HOME_DIR ... ;
    // open file as append "a"
    bool ifRet = initFile(outFile, outputFileName.c_str(), "a");
    if(!ifRet) {
        fprintf(stderr, "ERROR ... ");
        return false;
    }       

    // Get the topmost directory name
    size_t loc = dirCon.at(i).find_last_of("/");
    string dirName = dirCon.at(i).substr(loc+1, (dirCon.at(i).size()-(loc+1)));

    // Get the top directory content
    vector<string> subDirCon;
    bool subRetVal = FileSystem::getDirectoryContents(dirCon.at(i), &subDirCon);
    if(!subRetVal) { fprintf(stderr, "ERROR\n"); return false; }

    // Go through each file in directory, look for the one that matches
    for(int j = 0; j < subDirCon.size(); j++) {

        // Get filename
        loc = subDirCon.at(j).find_last_of("/");
        string fileName = subDirCon.at(j).substr(loc+1, (subDirCon.at(j).size()-(loc+1)));

        // If filename matches desired station, process and store
        if( fileName == string(dirName ...) ) {
            // Open File
            FILE *inFile;
            if(!initFile(inFile, subDirCon.at(j).c_str(), "r")) { 
                fprintf(stderr, "ERROR: ... !\n");
                break;
            }

            // Parse file line-by-line
            char str[TB_CHARLIMIT_LARGE];
            const char *delim = ",";
            while(true) {
                vector<string> splitString;
                fgets(str, TB_CHARLIMIT_LARGE, inFile);

                if(feof(inFile)) { break; }     // break at end of file
                removeEndLine(str);

                // If non-comment line, parse
                if(str[0] != COMCHAR){
                    string strString(str);
                    // remove end line char
                    strString.erase(std::remove(strString.begin(), strString.end(), '\n'), strString.end());
                    strcpy(str, strString.c_str());

                    char *temp = strtok(str,delim);
                    char *lastTemp;
                    while(temp != NULL) {
                        splitString.push_back(string(temp));
                        temp = strtok(NULL,delim);
                    }
                    if(splitString.size() > 0) { 
                        DateTime dtTemp(splitString.at(0));  
                        goodLines++;

                        /*  ... process splitString, use dtTemp ... */

                        // Output to file
                        fprintf(outFile, "%s\n", strFromStrVec(splitString).c_str());
                    }
                }
            } //while
            fclose(inFile); 
        }
    } //j
    cout << "GoodLines = " << goodLines << endl;

    fclose(outFile);
} // i

bool getDirectoryContents(const string dirName, vector<string> *conts) {
    path p(dirName);
    try {
        // Confirm Exists
        if(!exists(p)) {
            fprintf(stderr, "ERROR: '%s' does not exist!\n", dirName.c_str());
            return false;
        }

        // Confirm Directory
        if(!is_directory(p)) {
            return false;
        }

        conts->clear();

        // Store paths to sort later
        typedef vector<path> vec;
        vec v;

        copy(directory_iterator(p), directory_iterator(), back_inserter(v));

        sort(v.begin(), v.end()); 

        for(vec::const_iterator it(v.begin()), it_end(v.end()); it != it_end; ++it) {
            conts->push_back(it->string());
        }


    } catch(const filesystem_error& ex) {
        fprintf(stderr, "ERROR: '%s'!\n", ex.what());
        return false;
    }   

    return true;
}

解决方法:

没有更多信息可以继续,我想您要处理的是Schlemiel the Painter的算法:(Original)(Wikipedia).它们非常容易进行字符串处理.让我给你举个例子.

我想读取文件中的每一行,以某种方式处理每一行,并通过一些中间处理来运行它.然后,我想收集结果,然后将其写回到磁盘.这是一种方法.我犯了一个容易犯的巨大错误:

// proc.cpp
class Foo
{
  public:
  std::string chew_on(std::string const& line_to_chew_on) {...}
  ...
};

Foo processor;
std::string buffer;

// Read/process
FILE *input=fopen(..., "r");
char linebuffer[1000+1];
for (char *line=fgets(linebuffer, 1000, input); line; 
     line=fgets(linebuffer, 1000, input) ) 
{
    buffer=buffer+processor.chew_on(line);  //(1)
}
fclose(input);

// Write
FILE *output=fopen(...,"w");
fwrite(buffer.data(), 1, buffer.size(), output);
fclose(output);

乍一看很容易错过的问题是,每次运行第(1)行时,都会复制缓冲区的全部内容.如果有1000行,每行100个字符,那么您最终将花费时间来复制100 200 300 400….100,000 = 5,050,000字节副本来运行该行.增加到10,000行? 500500000.那个油漆罐越来越远了.

在此特定示例中,修复很容易.第(1)行应显示为:

    buffer.append(processor.chew_on(line)); // (2)

或等效地:(感谢Matthieu M.):

    buffer += processor.chew_on(line);

之所以能够提供帮助,是因为(通常)std :: string不需要制作缓冲区的完整副本即可执行附加功能,而在(1)中,我们坚持要制作一个副本.

更一般而言,假设(a)您正在执行的处理保持状态,(b)您经常引用全部或大部分状态,并且(c)状态随着时间的推移而增长.然后,很有可能您已经编写了一个Θ(n2)时间算法,该算法将精确显示您正在谈论的行为类型.

编辑

当然,股票回答“为什么我的代码慢?”是“运行个人资料”.有许多工具和技术可以做到这一点.一些选项包括:

> callgrind/kcachegrind(由David Schwartz建议)
> Random Pausing(由Mike Dunlavey建议)
> GNU探查器,gprof
> GNU测试覆盖率剖析器,gcov
> oprofile

他们都有自己的长处.尽管可能难以解释结果,但是“随机暂停”可能是最简单的实现. ‘gprof’和’gcov’在多线程程序上基本上是无用的. Callgrind彻底但缓慢,有时可能在多线程程序上发挥奇怪的作用. oprofile很快,可以很好地与多线程程序一起使用,但是可能很难使用,并且可能会遗漏一些东西.

但是,如果您试图分析一个单线程程序,并且正在使用GNU工具链进行开发,则gprof可能是一个不错的选择.拿我上面的proc.cpp.出于演示目的,我将介绍未优化的运行.首先,我重建程序以进行概要分析(将-pg添加到编译和链接步骤):

$g++ -O0 -g -pg -o proc.o -c proc.cpp
$g++ -pg -o proc proc.o

我运行该程序一次以创建性能分析信息:

./proc

除了执行通常的操作外,此运行还将在当前目录中创建一个名为“ gmon.out”的文件.现在,我运行gprof来解释结果:

$gprof ./proc
Flat profile:

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total           
 time   seconds   seconds    calls  ms/call  ms/call  name    
100.50      0.01     0.01   234937     0.00     0.00  std::basic_string<...> std::operator+<...>(...)
  0.00      0.01     0.00   234937     0.00     0.00  Foo::chew_on(std::string const&)
  0.00      0.01     0.00        1     0.00    10.05  do_processing(std::string const&, std::string const&)
...

是的,确实,我程序的时间的100.5%花费在std :: string运算符上.好吧,到了一些采样误差. (我正在VM中运行它……似乎gprof捕获的计时已关闭.我的程序运行所花的时间远远超过0.01累积秒…)

对于我的非常简单的示例,gcov的指导意义要小一些.但是,这就是它所显示的.首先,为gcov编译并运行:

$g++ -O0 -fprofile-arcs -ftest-coverage -o proc proc.cpp
$./proc
$gcov ./proc
...

这会在当前目录中创建一堆以.gcno,.gcda,.gcov结尾的文件. .gcov中的文件告诉我们在运行过程中每行代码执行了多少次.因此,在我的示例中,我的proc.cpp.gcov最终看起来像这样:

        -:    0:Source:proc.cpp
        -:    0:Graph:proc.gcno
        -:    0:Data:proc.gcda
        -:    0:Runs:1
        -:    0:Programs:1
        -:    1:#include 
        -:    2:#include 
        -:    4:class Foo
        -:    5:{
        -:    6:  public:
   234937:    7:  std::string chew_on(std::string const& line_to_chew_on) {return line_to_chew_on;}
        -:    8:};
        -:    9:
        -:   10:
        -:   11:
        1:   12:int do_processing(std::string const& infile, std::string const& outfile)
        -:   13:{
        -:   14:  Foo processor;
        2:   15:  std::string buffer;
        -:   16:
        -:   17:  // Read/process
        1:   18:  FILE *input=fopen(infile.c_str(), "r");
        -:   19:  char linebuffer[1000+1];
   234938:   20:  for (char *line=fgets(linebuffer, 1000, input); line; 
        -:   21:       line=fgets(linebuffer, 1000, input) ) 
        -:   22:    {
   234937:   23:      buffer=buffer+processor.chew_on(line);  //(1)
        -:   24:    }
        1:   25:  fclose(input);
        -:   26:
        -:   27:  // Write
        1:   28:  FILE *output=fopen(outfile.c_str(),"w");
        1:   29:  fwrite(buffer.data(), 1, buffer.size(), output);
        1:   30:  fclose(output);
        1:   31:}
        -:   32:
        1:   33:int main()
        -:   34:{
        1:   35:  do_processing("/usr/share/dict/words","outfile");
        -:   36:}

因此,从这里,我将不得不得出结论,第23行的std :: string :: operator(执行234,937次)是导致我的程序运行缓慢的潜在原因.

顺便说一句,callgrind / kcachegrind与多线程程序一起使用,并且可以提供更多得多的信息.对于此程序,我运行:

g++ -O0 -o proc proc.cpp
valgrind --tool=callgrind ./proc  # this takes forever to run
kcachegrind callgrind.out.*

而且我发现以下输出,表明真正消耗了我的周期的是很多存储副本(__memcpy_ssse3_back花费了99.4%的执行时间),我可以看到所有这些都发生在源代码的第23行以下:

上一篇:c-散列2D点的有效方法


下一篇:c-编译器优化问题