我将如何使用大小运算符 delete/delete[] 以及为什么它们更好?
How would I use the sized operators delete/delete[] and why are they better?
C++14引入"sized" versions of operator delete
,即
void operator delete( void* ptr, std::size_t sz );
和
void operator delete[]( void* ptr, std::size_t sz );
通读 N3536,似乎引入了这些运算符以提高性能。我知道 operator new
"stores" 使用的典型分配器是某处大容量内存的大小,这就是典型 operator delete
"knows" 需要多少内存才能 return 到免费商店。
不过我不确定为什么 operator delete
的 "sized" 版本会在性能方面有所帮助。唯一可以加快速度的是减少一次关于控制块大小的读取操作。这真的是唯一的优势吗?
第二,数组版本怎么处理? AFAIK,分配数组的大小不仅仅是 sizeof(type)*number_elements
,但可能会分配一些额外的字节,因为实现可能会将这些字节用作控制字节。在这种情况下,我应该将什么 "size" 传递给 operator delete[]
?能否提供一个简短的用法示例?
先解决你的第二个问题:
If present, the std::size_t size argument must equal the size argument passed to the allocation function that returned ptr.
因此,任何可能分配的额外 space 都是运行时库的责任,而不是客户端代码。
第一个问题比较难回答好。主要想法是(或者至少看起来是)块的大小通常不存储在块本身的旁边。在大多数情况下,块的大小会被写入,并且在块被释放之前不会再次写入。为避免数据在使用块时污染缓存,可以将其单独保存。然后当你去释放块时,大小经常会被调出到磁盘,所以读回它是相当慢的。
避免显式存储每个块的大小也很常见。分配器通常会为不同大小的块(例如,从 16 左右到大约几千字节左右的 2 的幂)有单独的池。它将从 OS 中为每个池分配一个(相当)大的块,然后将该大块的一部分分配给用户。当你传回一个地址时,它基本上会通过不同大小的池来搜索该地址,以找到它来自哪个池。如果你有很多池并且每个池中有很多块,那可能会比较慢。
这里的想法是避免这两种可能性。在典型情况下,您的 allocations/deallocations 无论如何都或多或少地与堆栈相关联,并且当它们达到您分配的大小时可能会在局部变量中。当你解除分配时,你通常会处于(或至少接近)与你进行分配的堆栈相同的级别,因此相同的局部变量将很容易获得,并且可能不会被调出到磁盘(或类似的东西)因为附近存储的其他变量也在使用中。对于非数组形式,对 ::operator new
的调用通常源自 new expression
,而对 ::operator delete
的调用则源自匹配的 delete expression
。在这种情况下,代码生成 construct/destroy 对象 "knows" 其将仅根据对象类型请求(和销毁)的大小 created/destroyed.
对于 C++14 operator delete
的 size
参数,您必须传递给 operator new
的相同大小,以字节为单位。但是正如您发现的那样,数组更复杂。为什么它更复杂,请参见此处:Array placement-new requires unspecified overhead in the buffer?
所以如果你这样做:
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比较好,好像是today it is actually not but the hope is that some standard libraries will improve performance of deallocation because they actually do not store the allocation size next to each allocated block (the classical/textbook model). For more on that, see here: Sized Deallocation Feature In Memory Management in C++1y
当然,不在每个分配旁边存储大小的原因是,如果您真的不需要它,那是一种浪费 space。对于进行许多小的动态分配(比它们应该更受欢迎!)的程序,这种开销可能很大。例如在 "plain vanilla" std::shared_ptr
构造函数中(而不是 make_shared
),一个引用计数是动态分配的,所以如果你的分配器存储它旁边的大小,它可能天真地需要大约 25%开销:分配器的一个 "size" 整数加上 four-slot control block。更不用说内存压力了:如果大小没有存储在分配的块旁边,你就可以避免在释放时从内存中加载一行——你唯一需要的信息在函数调用中给出(好吧,你还需要看看arena 或 free-list 或其他任何东西,但无论如何你都需要它,你仍然可以跳过一个负载)。
一些相关信息:目前 VS 17 的 sized delete[] 实现似乎已损坏。它总是 returns 通用指针大小 (void*)。 g++ 7.3.1 给出了完整数组的大小加上 8 字节的开销。还没有在其他编译器上测试过它,但是正如您所看到的,它们都没有给出预期的结果。
关于它的用途,正如所选答案中提到的那样,当您拥有自定义分配器时,主要用途就会发挥作用,要么传递给 stl 容器,要么仅用于本地内存管理。在这些情况下,将用户大小数组大小返回给您可能非常有用,这样您就可以从分配器中释放适当的大小。我可以看到可以避免使用它。下面是一段代码,可用于在编译器中测试 "correctness" of sized 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;
}
C++14引入"sized" versions of operator delete
,即
void operator delete( void* ptr, std::size_t sz );
和
void operator delete[]( void* ptr, std::size_t sz );
通读 N3536,似乎引入了这些运算符以提高性能。我知道 operator new
"stores" 使用的典型分配器是某处大容量内存的大小,这就是典型 operator delete
"knows" 需要多少内存才能 return 到免费商店。
不过我不确定为什么 operator delete
的 "sized" 版本会在性能方面有所帮助。唯一可以加快速度的是减少一次关于控制块大小的读取操作。这真的是唯一的优势吗?
第二,数组版本怎么处理? AFAIK,分配数组的大小不仅仅是 sizeof(type)*number_elements
,但可能会分配一些额外的字节,因为实现可能会将这些字节用作控制字节。在这种情况下,我应该将什么 "size" 传递给 operator delete[]
?能否提供一个简短的用法示例?
先解决你的第二个问题:
If present, the std::size_t size argument must equal the size argument passed to the allocation function that returned ptr.
因此,任何可能分配的额外 space 都是运行时库的责任,而不是客户端代码。
第一个问题比较难回答好。主要想法是(或者至少看起来是)块的大小通常不存储在块本身的旁边。在大多数情况下,块的大小会被写入,并且在块被释放之前不会再次写入。为避免数据在使用块时污染缓存,可以将其单独保存。然后当你去释放块时,大小经常会被调出到磁盘,所以读回它是相当慢的。
避免显式存储每个块的大小也很常见。分配器通常会为不同大小的块(例如,从 16 左右到大约几千字节左右的 2 的幂)有单独的池。它将从 OS 中为每个池分配一个(相当)大的块,然后将该大块的一部分分配给用户。当你传回一个地址时,它基本上会通过不同大小的池来搜索该地址,以找到它来自哪个池。如果你有很多池并且每个池中有很多块,那可能会比较慢。
这里的想法是避免这两种可能性。在典型情况下,您的 allocations/deallocations 无论如何都或多或少地与堆栈相关联,并且当它们达到您分配的大小时可能会在局部变量中。当你解除分配时,你通常会处于(或至少接近)与你进行分配的堆栈相同的级别,因此相同的局部变量将很容易获得,并且可能不会被调出到磁盘(或类似的东西)因为附近存储的其他变量也在使用中。对于非数组形式,对 ::operator new
的调用通常源自 new expression
,而对 ::operator delete
的调用则源自匹配的 delete expression
。在这种情况下,代码生成 construct/destroy 对象 "knows" 其将仅根据对象类型请求(和销毁)的大小 created/destroyed.
对于 C++14 operator delete
的 size
参数,您必须传递给 operator new
的相同大小,以字节为单位。但是正如您发现的那样,数组更复杂。为什么它更复杂,请参见此处:Array placement-new requires unspecified overhead in the buffer?
所以如果你这样做:
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比较好,好像是today it is actually not but the hope is that some standard libraries will improve performance of deallocation because they actually do not store the allocation size next to each allocated block (the classical/textbook model). For more on that, see here: Sized Deallocation Feature In Memory Management in C++1y
当然,不在每个分配旁边存储大小的原因是,如果您真的不需要它,那是一种浪费 space。对于进行许多小的动态分配(比它们应该更受欢迎!)的程序,这种开销可能很大。例如在 "plain vanilla" std::shared_ptr
构造函数中(而不是 make_shared
),一个引用计数是动态分配的,所以如果你的分配器存储它旁边的大小,它可能天真地需要大约 25%开销:分配器的一个 "size" 整数加上 four-slot control block。更不用说内存压力了:如果大小没有存储在分配的块旁边,你就可以避免在释放时从内存中加载一行——你唯一需要的信息在函数调用中给出(好吧,你还需要看看arena 或 free-list 或其他任何东西,但无论如何你都需要它,你仍然可以跳过一个负载)。
一些相关信息:目前 VS 17 的 sized delete[] 实现似乎已损坏。它总是 returns 通用指针大小 (void*)。 g++ 7.3.1 给出了完整数组的大小加上 8 字节的开销。还没有在其他编译器上测试过它,但是正如您所看到的,它们都没有给出预期的结果。 关于它的用途,正如所选答案中提到的那样,当您拥有自定义分配器时,主要用途就会发挥作用,要么传递给 stl 容器,要么仅用于本地内存管理。在这些情况下,将用户大小数组大小返回给您可能非常有用,这样您就可以从分配器中释放适当的大小。我可以看到可以避免使用它。下面是一段代码,可用于在编译器中测试 "correctness" of sized 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;
}