C++:在程序中查找最大的容器

5
我正在尝试分析一个大型C++程序。该程序大量使用STL容器数据结构,如set、map、unordered set、unordered map、vector等。有时它们是嵌套的,例如集合映射。
我想在程序的特定运行中找出哪些容器包含最多的元素(即size()的最大值)。我可以对程序进行一些小的编辑。
如果有一种方法可以迭代所有容器,或者拦截容器的(修改大小的)API,那可能会有所帮助。但这些都不可能。
你会如何处理这个问题?
附加说明:平台为Linux,编译器为g++或clang++。

5
我会使用像ValGrind或Purify这样的内存监测工具。 - SergeyA
1
你在哪个平台上?我会使用一个钩取全局内存分配器的工具来处理这个问题。Visual Studio 2015拥有强大的内置工具来分析内存分配,其他平台也存在类似的工具。 - mattnewport
@SergeyA:您能否提及一下在这里如何使用Valgrind? - Arun
@mattnewport:它是Linux。 - Arun
你的程序有多大?是数百万行还是只有几千行?你能花几周时间解决问题吗?(如果是这种情况,使用MELT定制你的编译器可能是值得的) - Basile Starynkevitch
3个回答

2

当您的项目非常大且有许多不同容器的实例时,此方法非常有用。该方法的优点是您不需要修改大量的代码。它允许您缩小要查找的容器类型。此方法有助于按容器和类型诊断情况。

可以重新定义template< class T > struct allocator。可以在std头文件中重命名原始分配器或修改它。这使得可以对分配和释放进行统计。您将知道每种元素类型的数量和大小。但是,您无法知道哪个具有元素的容器实例。

模板template< class T > struct allocator位于库头文件中。它始终存在,并且不需要重新构建开发环境库,因为据您所知,该模板不可能编译成静态库(除了特殊化)。模板始终与您的源代码一起编译。但是,预编译头文件可能会出现问题。对于项目,可以重新生成或不使用它,但对于库,则需要检查。可能这是该方法的瓶颈,但很容易验证是否存在问题。

有一种经验性的方法,不能保证准确性。当应用程序关闭时,容器在元素被释放之后被释放。因此,您可以编写父类型的每个容器的统计信息,了解哪种类型的容器中有多少内部元素。

例如,我们有:

vector<A>({1,2,3}) and map<string,B>({1,2}) and map<string,B>({1,2})

这将生成类似以下的释放事件列表:
B, B, map<string,B>,
A, A, map<string,A>,
A, A, A, vector<A>,

所以您可以知道,在vector<A>中有3个元素A,在map<string,A>中有2个元素A,在map<string,A>中也有2个元素A

更改标准头文件是不规范的。 - BitWhistler
1
合规的。当它能够解决足够困难的问题时,它只存在于开发环境中。如果它能够帮助解决问题并且回滚将尽快完成,那么在生产中也是合规的。如果更改标准不合规,则开发标准库、IDE和操作系统也不合规。 - oklas

2
如果你可以进行小的编辑,你能把每个容器添加到一个大列表中吗? 就像这样:
std::set<......> my_set;  // existing code
all_containers.add( &my_set ); // minor edit IMHO

然后你可以调用all_containers.analyse(),这将在每个容器上调用size()并打印结果。

你可以像这样使用:

struct ContainerStatsI {
  virtual int getSize() = 0;
};
template<class T> struct ContainerStats : ContainerStatsI {
  T* p_;
  ContainerStats( T* p ) : p_(p) {}
  int getSize() { return p->size(); }
};
struct ContainerStatsList {
  std::list<ContainerStatsI*> list_; // or some other container....
  template<class T> void add( T* p ) {
    list_.push_back( new ContainerStats<T>(p) );
  }
  // you should probably add a remove(T* p) as well
  void analyse() {
    for( ContainerStatsI* p : list_ ) {
      p->getSize(); // do something with what's returned here
    }
  }
};

可以使用宏,将创建容器的文件和行传递给all_containers。 - oklas
谢谢您的想法。添加ContainerStats*类是非常可能的,但为每个容器添加add()调用是令人敬畏的。我们如何处理嵌套容器,例如从int到set的映射? - Arun
容器名称可以使用宏生成,例如__FILE____LINE____COUNTER__。它们可以被折叠到宏包装器中,例如ADD(arg),该包装器调用add(arg)。对我来说,真正的挑战是在所有容器上调用add(),因为它们有成千上万个 :) - Arun
文件和行只是代码中的一个点。它可以创建许多相同的实例.... - BitWhistler
现在,我想你需要自动化调用“add”的功能。 我会在所有文件中执行查找和替换,将“map”更改为“my_map”等内容。然后你可以在构造函数/析构函数中进行“添加”和“删除”。这不是最好的做法,但以后很容易改为别名。 - BitWhistler
显示剩余2条评论

1
在std头文件中的容器析构函数中添加统计代码。这不需要修改大量的项目代码。但是这只显示容器类型(请参见我在此处的另一个回答)。该方法不需要C++0x或C++11或任何更高版本。
第一步和强制性步骤是将您的std库添加到源代码控制中,例如git,以便快速查看实际更改并快速在修改版和原始版本之间切换。
Stat类的声明放置在std库源文件夹中:
class Stat {
    std::map<std::string,int> total;
    std::map<std::string,int> maximum;
public:
    template<class T>
    int log( std::string cont, size_t size ) {
        std::string key = cont + ": " + typeid(T).name();
        if( maximum[key] < size ) maximum[key] = size;
        total[key] += size;
    }
    void show_result() {
        std::cout << "container type total maximum" << std::endl;
        std::map<std::string,int>::const_iterator it;
        for( it = total.begin(); it != total.end(); ++it ) {
            std::cout << it->first << " " << it->second
               << " " << maximum[it->first] << std::endl;
        }
    }
    static Stat& instance();
    ~Stat(){ show_result(); }
};

在您的项目cpp文件中实例化Stat类的单例:

Stat& Stat::instance() {
    static Stat stat;
    return stat;
}

编辑标准库容器模板。在析构函数中添加统计记录。

// modify this standart template library sources:

template< T, Allocator = std::allocator<T> > vector {
    ...
    virtual ~vector() {
        Stat::instance().log<value_type>( "std::vector", this->size() );
    }
};

template< Key, T, Compare = std::less<Key>,
    Allocator = std::allocator<std::pair<const Key, T> > map {
    ...
    virtual ~map(){
        Stat::instance().log<value_type>( "std::map", this->size() );
    }
};

考虑一个编程示例程序:

Consider a program for example now:

int main() {
    {
        // reject to use C++0x, project does not need such dependency
        std_vector<int> v1; for(int i=0;i<10;++i) v1.push_back( i );
        std_vector<int> v2; for(int i=0;i<10;++i) v2.push_back( i );
        std_map<int,std::string> m1; for(int i=0;i<10;++i) m1[i]="";
        std_map<int,std::string> m2; for(int i=0;i<20;++i) m2[i]="";
    }
    Stat::instance().show_result();
    return 0;
}

gcc的结果如下:

container type total maximum
std::map: St4pairIiSsE 30 20
std::vector: i 20 10
如果您需要更详细的类型描述信息,则可以查找与您的开发环境相关的信息。此处描述了gcc的转换:https://lists.gnu.org/archive/html/help-gplusplus/2009-02/msg00006.html 输出可能如下所示:
container type total maximum
std::map: std::pair<int, std::string> 30 20
std::vector: int 20 10

也许可以实现,但这是最不优雅的方法。 - BitWhistler
1
真的很不专业,说话时没有提供任何论据。 - oklas
请注意您的静态数据。在 Stat 实例被销毁后,这些数据可能会被销毁。有时候,这些数据对于 Stat 来说可能是“不可见”的。 - oklas
抱歉@oklas。您可以随意更改标准头文件。但我不会这样做,因为我只会进行我能够保留和部署的更改,而这不是我会保留的更改,因为gcc/libc版本变化,各种盒子有不同的目录布局,更不用说不同的操作系统等等。 - BitWhistler
不要混淆项目的具体问题和标准库分发。无论如何都没有任何虚幻的服务器部署。如果你不能走这条路,就不要走它。 - oklas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接