在C ++中优化冗余字符串分配


10

我有一个相当复杂的C ++组件,其性能已成为问题。分析表明,大多数执行时间只是花在为std::strings 分配内存上。

我知道这些字符串之间有很多冗余。少数值非常频繁地重复,但也有很多唯一值。字符串通常很短。

我现在只是在考虑以某种方式重用那些频繁的分配是否有意义。代替1000个指向1000个不同的“ foobar”值的指针,我可以有1000个指向一个“ foobar”值的指针。这样可以提高内存效率,这是一个不错的选择,但是我在这里最担心的是延迟。

我猜一个选择是维护某种已经分配了值的注册表,但是是否有可能使注册表查找比冗余内存分配更快?这是可行的方法吗?


6
可行?是的,当然可以-其他语言也可以常规执行此操作(例如Java-搜索String interning)。但是,要考虑的重要一件事是,缓存的对象必须是不可变的,而std :: string则不是。
绿巨人

2
这个问题更相关:stackoverflow.com/q/26130941
rwong

8
您是否分析了哪种类型的字符串操作主导您的应用程序?它是复制,子字符串提取,串联,逐字符操作吗?每种类型的操作都需要不同的优化技术。另外,请检查您的编译器和标准库实现是否支持“小字符串优化”。最后,如果使用字符串实习,则哈希函数的性能也很重要。
rwong

2
你在用那些弦做什么?它们只是用作某种标识符或密钥吗?还是将它们合并以创建一些输出?如果是这样,您如何进行字符串连接?使用+运算符还是使用字符串流?琴弦从哪里来?您的代码或外部输入中的文字?
阿蒙

Answers:


3

正如Basile所建议的那样,我在很大程度上依赖于内部字符串,其中字符串查找转换为32位索引以进行存储和比较。在我的情况下,这很有用,因为有时我有成千上万个具有名为“ x”的属性的组件,例如,由于脚本编写者经常按名称访问它,所以仍需要使用用户友好的字符串名称。

我使用trie进行查找(也进行了实验,unordered_map但由内存池支持的经过调整的trie至少开始表现更好,并且更容易使线程安全,而不必每次访问结构时都锁定),但事实并非如此快速建造创造std::string。关键是要加快后续操作的速度,例如检查字符串是否相等,就我而言,这归结为检查两个整数是否相等并大幅度减少内存使用量。

我猜一个选择是维护某种已经分配了值的注册表,但是是否有可能使注册表查找比冗余内存分配更快?

要比单个数据结构更快地搜索数据结构将非常困难 malloc,例如,如果您要从外部输入(例如文件)中读取大量字符串,那么我的诱惑是如果可能的话,使用顺序分配器。缺点是您无法释放单个字符串的内存。分配器池中的所有内存必须立即释放或完全不释放。但是,在只需要以直接的顺序方式分配大量可变大小的微小内存块的情况下,顺序分配器会很方便,之后再将其扔掉。我不知道这是否适用于您的情况,但在适用时,它可能是修复与频繁的小内存分配有关的热点的简便方法(这可能与缓存未命中和页面错误有关,而不是与底层问题有关。的算法malloc)。

在没有顺序分配器约束的情况下,固定大小的分配更容易加速,因为顺序分配器约束阻止您释放特定的内存块以供以后重用。但是,使可变大小的分配比默认分配器更快是非常困难的。基本上,malloc如果不应用限制其适用性的约束,则使任何一种内存分配器都比通常非常困难的内存分配器快。一种解决方案是对所有长度等于或小于8字节的字符串使用固定大小的分配器,如果您有很多负载的话,则较长的字符串很少见(您可以使用默认分配器)。这确实意味着1个字节的字符串浪费了7个字节,但是它应该消除与分配相关的热点,例如,如果95%的时间中您的字符串很短。

我刚刚想到的另一种解决方案是使用展开的链表,这听起来可能很疯狂,但是却听到了我的声音。

在此处输入图片说明

这里的想法是使每个展开的节点为固定大小而不是可变大小。当您执行此操作时,可以使用非常快速的固定大小的块分配器来池化内存,为链接在一起的可变大小的字符串分配固定大小的块。那不会减少内存的使用,由于链接的成本,它会增加内存的使用量,但是您可以使用展开的大小来找到适合您需求的平衡。这是个古怪的主意,但是应该消除与内存相关的热点,因为您现在可以有效地池化已分配在庞大连续块中的内存,并且仍然具有分别释放字符串的好处。这是我写的一个简单的ol'固定分配器(我为别人制作的一个示例,没有生产相关的绒毛),您可以自由使用:

#ifndef FIXED_ALLOCATOR_HPP
#define FIXED_ALLOCATOR_HPP

class FixedAllocator
{
public:
    /// Creates a fixed allocator with the specified type and block size.
    explicit FixedAllocator(int type_size, int block_size = 2048);

    /// Destroys the allocator.
    ~FixedAllocator();

    /// @return A pointer to a newly allocated chunk.
    void* allocate();

    /// Frees the specified chunk.
    void deallocate(void* mem);

private:
    struct Block;
    struct FreeElement;

    FreeElement* free_element;
    Block* head;
    int type_size;
    int num_block_elements;
};

#endif

#include "FixedAllocator.hpp"
#include <cstdlib>

struct FixedAllocator::FreeElement
{
    FreeElement* next_element;
};

struct FixedAllocator::Block
{
    Block* next;
    char* mem;
};

FixedAllocator::FixedAllocator(int type_size, int block_size): free_element(0), head(0)
{
    type_size = type_size > sizeof(FreeElement) ? type_size: sizeof(FreeElement);
    num_block_elements = block_size / type_size;
    if (num_block_elements == 0)
        num_block_elements = 1;
}

FixedAllocator::~FixedAllocator()
{
    // Free each block in the list, popping a block until the stack is empty.
    while (head)
    {
        Block* block = head;
        head = head->next;
        free(block->mem);
        free(block);
    }
    free_element = 0;
}

void* FixedAllocator::allocate()
{
    // Common case: just pop free element and return.
    if (free_element)
    {
        void* mem = free_element;
        free_element = free_element->next_element;
        return mem;
    }

    // Rare case when we're out of free elements.
    // Create new block.
    Block* new_block = static_cast<Block*>(malloc(sizeof(Block)));
    new_block->mem = malloc(type_size * num_block_elements);
    new_block->next = head;
    head = new_block;

    // Push all but one of the new block's elements to the free stack.
    char* mem = new_block->mem;
    for (int j=1; j < num_block_elements; ++j)
    {
        void* ptr = mem + j*type_size;
        FreeElement* element = static_cast<FreeElement*>(ptr);
        element->next_element = free_element;
        free_element = element;
    }
    return mem;
}

void FixedAllocator::deallocate(void* mem)
{
    // Just push a free element to the stack.
    FreeElement* element = static_cast<FreeElement*>(mem);
    element->next_element = free_element;
    free_element = element;
}


0

很久以前,在编译器构建中,我们使用了一种称为“数据椅子”(而不是数据库,即DB的德语口语翻译)。这只是为字符串创建了一个哈希,然后将其用于分配。因此,任何字符串都不是堆/堆栈上的一部分内存,而是该数据椅子中的哈希码。您可以用String此类代替。但是需要大量的代码返工。当然,这仅适用于R / O字符串。


那写时复制呢。如果更改字符串,则将重新计算哈希并恢复它。还是那行不通?
杰里·耶利米

@JerryJeremiah取决于您的应用程序。您可以更改哈希表示的字符串,并且在检索哈希表示时,将获得新值。在编译器上下文中,您将为新字符串创建新哈希。
qwerty_so

0

请注意,内存分配和使用的实际内存如何与性能低下有关:

当然,实际分配内存的成本非常高。因此,std :: string可能已经在小型字符串中使用了就地分配,因此实际分配的数量可能比您最初假设的要少。如果此缓冲区的大小不够大,则可能会受到使用23个字符的Facebook字符串类(https://github.com/facebook/folly/blob/master/folly/FBString.h)的启发。内部分配之前。

使用大量内存的成本也值得注意。这也许是最大的罪魁祸首:您的计算机中可能有足够的RAM,但是,缓存大小仍然足够小,以至于在访问尚未缓存的内存时会损害性能。您可以在这里阅读有关此内容:https : //en.wikipedia.org/wiki/Locality_of_reference


0

除了使字符串操作更快以外,另一种方法是减少字符串操作的数量。例如,是否可以用枚举替换字符串?

在Cocoa中使用了另一种可能有用的方法:在某些情况下,您有成百上千的词典,而它们的键大多相同。因此,它们使您可以创建一个由一组字典键组成的对象,并且有一个字典构造函数将此类对象作为参数。该词典的行为与任何其他词典相同,但是当您在该键集中添加带有键的键/值对时,该键不会重复,而只会存储指向键集中的键的指针。因此,成千上万的词典只需要该集中每个键字符串的一个副本。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.