繁体   English   中英

我如何使用大小的运算符删除/删除[],为什么它们更好?

[英]How would I use the sized operators delete/delete[] and why are they better?

C ++ 14引入了“大小”的operator delete版本 ,即

void operator delete( void* ptr, std::size_t sz );

void operator delete[]( void* ptr, std::size_t sz );

通过N3536阅读,似乎引入了这些运营商以提高性能。 我知道operator new使用的典型分配器“存储”大容量内存的大小,这就是典型的operator delete “知道”返回到免费存储的内存量。

我不确定为什么“大小”版本的operator delete在性能方面会有所帮助。 唯一可以加快速度的是从控制块开始减少关于大小的读取操作。 这确实是唯一的优势吗?

其次,我该如何处理阵列版本? AFAIK,分配的数组的大小不仅仅是sizeof(type)*number_elements ,但可能会分配一些额外的字节,因为实现可能会将这些字节用作控制字节。 在这种情况下,我应该将“大小”传递给operator delete[] 你能提供一个简短的使用例子吗?

首先处理你的第二个问题:

如果存在,则std :: size_t size参数必须等于传递给返回ptr的分配函数的size参数。

因此,可能分配的任何额外空间是运行时库的责任,而不是客户端代码。

第一个问题更难以回答。 主要想法是(或者至少似乎是)块的大小通常不存储在块本身的旁边。 在大多数情况下,会写入块的大小,并且在取消分配块之前不会再次写入。 为了避免在使用块时数据污染高速缓存,可以单独保存。 然后,当你去解除块时,大小将经常被分页到磁盘,所以重新读取它是非常慢的。

避免明确地显式存储每个块的大小也是相当普遍的。 分配器通常具有用于不同大小的块的单独池(例如,从16左右到大约几千字节左右的2的幂)。 它将从操作系统为每个池分配一个(相当)大块,然后将该大块的块分配给用户。 当你传回一个地址时,它基本上通过不同大小的池搜索该地址,以找到它来自哪个池。 如果每个池中有很多池和很多块,那可能会相对较慢。

这里的想法是避免这两种可能性。 在典型情况下,您的分配/解除分配或多或少地与堆栈相关联,并且当它们是您分配的大小时,可能是在本地变量中。 当您取消分配时,您通常会(或至少接近)与分配位置相同的堆栈级别,因此相同的本地变量将很容易获得,并且可能不会被分页到磁盘(或类似的东西)因为附近存储的其他变量也在使用中。 对于非数组形式,对::operator new的调用通常源于一个new expression ,并且从匹配的delete expression调用::operator delete 在这种情况下,生成用于构造/销毁对象的代码“仅仅根据正在创建/销毁的对象的类型”“知道”它将要请求(和销毁)的大小。

对于C ++ 14 operator deletesize参数,您必须传递与operator new相同的大小,以字节为单位。 但是你发现它对于数组来说更复杂。 为什么它更复杂,请看这里: 数组placement-new需要缓冲区中未指定的开销?

所以,如果你这样做:

std::string* arr = new std::string[100]

执行此操作可能无效:

operator delete[](arr, 100 * sizeof(std::string)); # BAD CODE?

因为原始的new表达式等同于:

std::string* arr = new (new char[100 * sizeof(std::string)]) std::string[100];

至于为什么大小的delete API更好,似乎今天它实际上不是,但希望是一些标准库将提高解除分配的性能,因为它们实际上不存储每个分配块旁边的分配大小(经典/教科书)模型)。 有关详细信息,请参阅此处: C ++ 1y中内存管理中的大小重新分配功能

当然,不在每个分配旁边存储大小的原因是,如果你不真正需要它,那就是浪费空间。 对于进行许多小动态分配的程序(比它们应该更受欢迎!),这种开销可能很大。 例如,在“普通vanilla” std::shared_ptr构造函数(而不是make_shared )中,引用计数是动态分配的,因此如果您的分配器存储它旁边的大小,它可能天真地需要大约25%的开销:一个“大小”分配器的整数加上四时隙控制块 更不用说内存压力了:如果大小没有存储在分配的块旁边,你可以避免在重新分配时从内存中加载一行 - 你需要的唯一信息是在函数调用中给出的(好吧,你还需要看看竞技场或自由列表或其他什么,但在任何情况下你都需要它,你仍然可以跳过一个负载)。

一些相关信息:目前VS 17的大小删除[]的实现似乎已经破解。 它总是返回通用指针大小(void *)。 g ++ 7.3.1给出了完整数组的大小加上8字节的开销。 没有在其他编译器上测试它,但是你看到它们都没有给出预期的结果。 如所选择的答案中所提到的,WRT对它的实用性有用,当您拥有自定义分配器时,主要用处就会发挥作用,要么传递给stl容器,要么仅用于本地内存管理。 在这些情况下,将用户大小的数组大小返回给您可能非常有用,因此您可以从分配器中释放适当的大小。 我可以看到它可以避免使用它。 这是一个代码,您可以使用它来测试编译器中大小的delete []实现的“正确性”:

#include <iostream>
#include <sstream>

#include <string>

std::string true_cxx =

#ifdef __clang__
"clang++";
#elif _MSC_VER
"MVC";
#else
"g++";
#endif

std::string ver_string(int a, int b, int c) {
    std::ostringstream ss;
    ss << a << '.' << b << '.' << c;
    return ss.str();
}


std::string true_cxx_ver =
#ifdef __clang__
ver_string(__clang_major__, __clang_minor__, __clang_patchlevel__);
#elif _MSC_VER
#ifdef _MSC_FULL_VER
#if   _MSC_FULL_VER == 170060315
"MSVS 2012; Platform Toolset v110";
#elif _MSC_FULL_VER == 170051025
"MSVS 2012; Platform Toolset v120_CTP_Nov2012";
#elif _MSC_FULL_VER == 180020617
"MSVS 2013; Platform Toolset v120";
#elif _MSC_FULL_VER == 191426431
"MSVS 2017; Platform Toolset v140";
#else
"Not recognized";
#endif
#endif // _MSC_FULL_VER
#else
ver_string(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__);
#endif


// sized class-specific deallocation functions
struct X {
    static void operator delete(void* ptr, std::size_t sz)
    {
        std::cout << "custom delete for size " << sz << '\n';
        ::operator delete(ptr);
    }
    static void operator delete[](void* ptr, std::size_t sz)
    {
        std::cout << "custom delete[] for size " << sz << '\n';
        ::operator delete(ptr);
    }
    char16_t c[2];
};
int main() {
    X* p1 = new X;
    delete p1;
    X* p2 = new X[10];
    for (int i = 0; i < 10; ++i)
        p2[i] = X{ (char16_t)i };
    std::cout << "Compiler: "<<true_cxx.c_str()<<": Version:" << true_cxx_ver.c_str() << std::endl;
    delete[] p2;
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM