55用d编程管理内存
管理内存
D不显式管理内存.本章为垃集
,可以研究std.allocator
及各种管理内存方法.
取相邻
变量地址
import std.stdio;
void main() {
int i;
int j;
writeln("i: ", &i);
writeln("j: ", &j);
}
D的动态变量放在垃集
内存块上.当不用变量了,垃集根据适当算法终止它
算法
大致为:扫描所有指针/引用直接/间接可达
的内存块.可达标记为在用,其余未用.终止
不可访问内存块的对象和结构,他们可供未来使用.定义根为每个线程的所有程序栈,通过GC.addRoot或GC.addRange
添加所有全局,线程本地变量,和额外数据.
一些垃集算法可以聚拢内存,为保持正确,所有指向这些对象的指针和引用都得更新,D不这样做.
如果精确知道哪块内存包含指针,哪块不包含,则叫精确垃集
.如果按指针扫描所有指针,则叫保守垃集
.D的垃集是部分保守的,只扫描含指针块,但会扫描这些块的所有数据.因此,有时都没收集有些块,而泄露
内存.大块容易出错,有时建议人工释放未用大块,以避免出错.
未指定终结器的执行顺序.有时引用成员的对象可能释放在包含这个成员的对象前释放.即本对象含有已释放对象成员的引用
.因而,在析构器中,不要访问指向动态变量的类成员引用.这与c++确定性的内存析构顺序
不一样.
可能有各种原因启动垃集.如需要更多空间.依赖于垃集的实现,在垃集循环中分配新对象,会冲突垃集进程自身.在收集循环过程中,所有线程都得停止.因为你地址
变了.就像一切停止了.
多数时候,程序员不需要干预垃集
,但可用在core.memory
中定义的函数延迟和分发
垃集循环.
启动和延迟垃集周期
GC.disable();//禁止垃集周期
//性能重要区
GC.enable();//允许垃集
然而GC.disable()
如果垃集要获取更多内存时,不保证执行时避免不垃集.它仍然要取内存并跑垃集.
除了自动,你还可显式调用垃集循环:GC.collect()
import core.memory;
// ...
GC.collect();//收集
垃集一般不返回内存块给操作系统,以供未来使用.但可用GC.minimize();
退回一部分.
分配内存.
ubyte[100] buffer;
,固定数组缓冲.
还可以为void
型.void[100] buffer = void;
void型
无法分配.init
,所以必须赋初值void
.
我们仅使用core.memory(还有其他各种有用特征)中的GC.calloc
来保留内存.
也可用在std.c.stdlib
中的分配内存函数.
import core.memory;
// ...
void * buffer = GC.calloc(100);//0值填充内存
//并返回首地址.
可以int * intBuffer = cast(int * )buffer;
但一般:
int * intBuffer = cast(int*)GC.calloc(100);
一般这样:
int * intBuffer = cast(int*)GC.calloc(int.sizeof * 25);
类的区别:类变量与类对象的大小不一样.
.sizeof
是类变量大小,一般==size_t
,64位则为8,32位则为4.类对象实际大小这样取:_ _traits(classInstanceSize)
MyClass * buffer = cast(MyClass*)GC.calloc(__traits(classInstanceSize, MyClass) * 10);//10个
void * buffer = GC.calloc(10_000_000_000);
空间不够时,抛core.exception.OutOfMemoryError
异常.
返回用GC.free(buffer);
//释放.
要显式消灭.对每个变量显式调用destroy()
函数.
在垃集收集与释放过程中对类/构
采取不同内部机制来调用终止器.
最好的方法是用new
来确保调用析构,这样的话GC.free
会调用析构器.
void * oldBuffer = GC.calloc(100);
// ...
void * newBuffer = GC.realloc(oldBuffer, 200);
程序觉得先前内存不够了,就再分配
,返回一个新地址.
1,如果旧区域后面能够分配足够内存.则原位扩展.
2,旧区域后面已用或内存不够,则分配到新的更大的区域,并把源数据复制过去.
3,可给旧区域参数赋值为null
,此时简单分配新内存.
4,比旧区域小,旧区域的剩余部分返回垃集.
5,大小置为0,此时简单释放内存.
GC.realloc
改编自C的
重分配.太复杂.接口不好.
GC.realloc
令人惊讶的一点:即使用GC.calloc
分配了原内存,也不清理扩展部分.因而当为0初化内存时,reallocCleared
很有用.
import core.memory;
/*像GC.realloc一样工作,扩展内存则清除额外字节.*/
void * reallocCleared(
void * buffer,
size_t oldLength,
size_t newLength,
GC.BlkAttr blockAttributes = GC.BlkAttr.NONE,
const TypeInfo typeInfo = null) {
/*将实际工作分派给GC.realloc.*/
buffer = GC.realloc(buffer, newLength,blockAttributes, typeInfo);
//扩展,则清理额外字节
if (newLength > oldLength) {
import std.c.string;
auto extendedPart = buffer + oldLength;
auto extendedLength=newLength - oldLength;
memset(extendedPart, 0, extendedLength);
//清理,从扩展部分,到长度为0的赋值
}
return buffer;
}
用std.c.string
中的memset
来清理新分配内存
memset
用一个指针和长度来指定给定内存的值
GC.extend
也类似,它仅应用上面的第1项,如果内存不能原位显式扩展,什么都不做直接返回0,
内存块属性
GC.calloc
和其他分配函数的可选属性BlkAttr
,
NONE
,无属性.
FINALIZE
,应终止内存块中对象.
垃集假定由程序员控制显式分配内存的对象的生命期,垃集并不终止这些内存区的对象.GC.BlkAttr.FINALIZE
用于请求垃集执行对象的析构器.
Class * buffer = cast(Class*)GC.calloc( __traits(classInstanceSize, Class) * 10, GC.BlkAttr.FINALIZE);
注意.FINALIZE
依赖块上正确的实现细节.强烈建议垃集
注意new
分配的细节.
NO_SCAN
,指定垃集不扫描这片内存区.
一个区的字节值像指向其他块不相关对象的指针.这时,即使他们实际生命期已结束,垃集假定他们还在用.
标记一个不含任何对象指针的内存块为GC.BlkAttr.NO_SCAN
,
int * intBuffer =cast(int * )GC.calloc(100, GC.BlkAttr.NO_SCAN);
内存块的整
值可为任意值,而不担心认错为指针.
NO_MOVE
,不应移动内存块
中对象.
APPENDABLE
,D运行时内部标志,加速快速附加,分配内存时,你最好别用.
NO_INTERIOR
,指定仅存在块首地址的指针.这允许减少错误指针,因为当跟踪指针时不计数中间块指针.
多个标志用|
:
const attributes =GC.BlkAttr.NO_SCAN | GC.BlkAttr.NO_INTERIOR;
一般,垃集只知道自己函数保留的内存块,并仅扫描他们.
它不知道std.c.stdlib.calloc
分配的内存块.
GC.addRange
用于引入不相关内存块,在用std.c.stdlib.free
释放他们前用GC.removeRange
.
有时,没有到垃集内存块的引用,如仅有的引用在c库中,垃集不知道引用,并假定该内存块不再用了.
GC.addRoot
按根引入内存块,收集循环时将扫描他们.
通过那个内存块直接/间接可达的变量,都标记为是活的.
当不在用内存块时调用GC.removeRoot
.
扩展存储区示例
struct Array(T) {
T * buffer; // 内存区
size_t capacity; // 容量
size_t length; // 长度
T element(size_t index) {//返回指定元素
import std.string;
enforce(index < length,format("Invalid index %s", index));
return *(buffer + index);
}
void append(T element) {//追加至尾
writefln("附加元素%s", length);
if (length == capacity) {
//没有新元素空间,赶紧加
size_t newCapacity = capacity + (capacity / 2) + 1;
increaseCapacity(newCapacity);
}
*(buffer + length) = element;++length;
//在尾放元素.
}
void increaseCapacity(size_t newCapacity) {
writefln("从%s到%s增加元素",capacity, newCapacity);
size_t oldBufferSize = capacity * T.sizeof;
size_t newBufferSize = newCapacity * T.sizeof;
buffer = cast(T*)reallocCleared(buffer, oldBufferSize, newBufferSize,GC.BlkAttr.NO_SCAN);
//该内存块不扫描指针
capacity = newCapacity;
}
}
简单数组
双精类型
import std.stdio;
import core.memory;
import std.exception;
// ...
void main() {
auto array = Array!double();
const count = 10;
foreach (i; 0 .. count) {
double elementValue = i * 1.1;
array.append(elementValue);
}
writeln("The elements:");
foreach (i; 0 .. count) {
write(array.element(i), ' ');
}
writeln();
}
对齐,整
对齐为4,未对齐的内存地址,更慢,导致线程错误,某些类型仅在对齐地址上工作.
.alignof
为默认对齐值.对类,是类变量,不是变对象(实体),类对象的对齐用std.traits.classInstanceAlignment
.
import std.stdio;
import std.meta;
import std.traits;
struct EmptyStruct {
}
struct Struct {
char c;
double d;
}
class EmptyClass {
}
class Class {
char c;
}
void main() {
alias Types = AliasSeq!(char, short, int, long,
double, real,
string, int[int], int*,
EmptyStruct, Struct,
EmptyClass, Class);
writeln(" Size Alignment Type\n",
"=========================");
foreach (Type; Types) {
static if (is (Type == class)) {
size_t size = __traits(classInstanceSize, Type);
size_t alignment = classInstanceAlignment!Type;
} else {
size_t size = Type.sizeof;
size_t alignment = Type.alignof;
}
writefln("%4s%8s %s",
size, alignment, Type.stringof);
}
}
打印不同类型的对齐方式.不同环境可能不同:
Size Alignment Type
=========================
1 1 char
2 2 short
4 4 int
8 8 long
8 8 double
16 16 real
16 8 string
8 8 int[int]
8 8 int*
1 1 EmptyStruct
16 8 Struct
16 8 EmptyClass
17 8 Class
为了正确与效率
,必须在匹配他们对齐的地址上构建对象.
(candidateAddress + alignmentValue - 1) / alignmentValue * alignmentValue
//假定为整,且都截断了.
可放对象的最近地址值,
T * nextAlignedAddress(T)(T * candidateAddr) {
import std.traits;
static if (is (T == class)) {
const alignment = classInstanceAlignment!T;
} else {
const alignment = T.alignof;
}
const result = (cast(size_t)candidateAddr + alignment - 1)
/ alignment * alignment;
return cast(T*)result;
}
从模板参数中推导出类型,由于不可能是void *
,必须显式提供void *
的重载.
void * nextAlignedAddress(T)(void * candidateAddr) {
return nextAlignedAddress(cast(T*)candidateAddr);
}
//简单转发至上面代码
当用emplace
原位构造时,有用.
size_t sizeWithPadding(T)() {
static if (is (T == class)) {
const candidateAddr = __traits(classInstanceSize, T);
} else {
const candidateAddr = T.sizeof;
}
return cast(size_t)nextAlignedAddress(cast(T*)candidateAddr);
}
统计包含间隙(padding)
的对象的大小
.offsetof
属性
struct A {
byte b; // 1字节
int i; // 4字节
ubyte u; // 1字节
}
static assert(A.sizeof == 12);
有间隙.
.offsetof
.对象从头到本变量的距离
打印类型布局,用.offsetof
决定间隙类型.
void printObjectLayout(T)()
if (is (T == struct) || is (T == union)) {
import std.stdio;
import std.string;
writefln("=== Memory layout of '%s'" ~
" (.sizeof: %s, .alignof: %s) ===",
T.stringof, T.sizeof, T.alignof);
void printLine(size_t offset, string info) {
//打印单行布局信息
writefln("%4s: %s", offset, info);
}
//已观察内边距,打印其信息
void maybePrintPaddingInfo(size_t expectedOffset,
size_t actualOffset) {
if (expectedOffset < actualOffset) {
//有间隙.不一样
const paddingSize = actualOffset - expectedOffset;
printLine(expectedOffset, format("... %s-byte PADDING", paddingSize));
}
}
//如下个成员无间隙的预期偏移
size_t noPaddingOffset = 0;
//`__traits(allMembers)`是类型成员名`串`
foreach(memberName; __traits(allMembers, T)) {
mixin (format("alias member = %s.%s;",
T.stringof, memberName));
const offset = member.offsetof;
maybePrintPaddingInfo(noPaddingOffset, offset);
const typeName = typeof(member).stringof;
printLine(offset,
format("%s %s", typeName, memberName));
noPaddingOffset = offset + member.sizeof;
}
maybePrintPaddingInfo(noPaddingOffset, T.sizeof);
}
使用
struct A {
byte b;
int i;
ubyte u;
}
void main() {
printObjectLayout!A();
}
一个最小化的技术是从大到小排序成员.
struct B {//上移
int i;byte b;ubyte u;
}
void main() {
printObjectLayout!B();
}
align
属性.用于指定变量,用户定义类型,及其成员的
对齐.
align (2)//'S'对象对齐,2字节对齐边界
struct S {
byte b;
align (1) int i; //成员i的对齐,按1字节对齐
ubyte u;
}//1字节,则无间隙.
//0,1,5-6.刚好6字节大小,无间隙了
void main() {
printObjectLayout!S();
}
虽然对齐
可减小大小,但不能满足默认对齐时,性能损失很大.一些cpu上,用未对齐数据,可能会崩溃.
可直接对变量指定对齐如align (32) double d;
.
但new
分配对象必须为size_t
的整数倍.这是垃集
要求的.否则未定义行为.
在特定内存位置
构造变量.
新
要完成:
1,内存足够大,新内存区为原始的,不与任何系统/对象关联.
2,在内存位置调用对象构造器,此后,对象被放在内存上.
3,配置内存块,使有必要标志和基础设施来正确的释放对象
第1可用类似GC.calloc
显式完成,第2也可以.
可以在指定位置用std.conv.emplace
构造变量.
import std.conv;
// ...
emplace(address,...);//位置,构造参数...
未明确指定构/类
类型,因为emplace
可从指针位置推导出来类型.
Student * objectAddr = nextAlignedAddress(candidateAddr);
// ...
emplace(objectAddr, name, id);
根据指针类型推导出对象类型
import std.stdio;
import std.string;
import core.memory;
import std.conv;
// ...
struct Student {
string name;
int id;
string toString() {
return format("%s(%s)", name, id);
}
}
void main() {
/* 此类型的一些信息. */
writefln("Student.sizeof: %#x (%s) bytes",
Student.sizeof, Student.sizeof);
writefln("Student.alignof: %#x (%s) bytes",
Student.alignof, Student.alignof);
string[] names = [ "Amy", "Tim", "Joe" ];
auto totalSize = sizeWithPadding!Student() * names.length;
/* 为所有学生对象保留空间
*警告!还未构造通过此切片可访问的对象,正确构造前,不要访问他们*/
Student[] students =
(cast(Student*)GC.calloc(totalSize))[0 .. names.length];
foreach (int i, name; names) {
Student * candidateAddr = students.ptr + i;
Student * objectAddr =
nextAlignedAddress(candidateAddr);
writefln("address of object %s: %s", i, objectAddr);
const id = 100 + i;
emplace(objectAddr, name, id);
}
writeln(students);//都构造好了,可用了.
}
类变量不一定是类实体的精确类型.如动物
类变量,可引用猫
对象.因而原位
不能从指针
决定对象类型
.
因而必须显式指定类型.注意,类指针是类变量的指针,而不是类对象的指针.因而,指定实际类型允许程序员原位类对象/类变量.
必须用以下语法按void[]切片
指定类对象的内存位置.
Type variable=emplace!Type(voidSlice,构造参数...)
按
void[]切片原位
在切片上构造类对象,并返回它的类变量(引用).
interface Animal {
string sing();
}
class Cat : Animal {
string sing() {
return "meow";
}
}
class Parrot : Animal {
string[] lyrics;
this(string[] lyrics) {
this.lyrics = lyrics;
}
string sing() {
//std.algorithm.joiner用指定分隔符合并区间元素
return lyrics.joiner(", ").to!string;
}
}
在动物
层次上原位
对象.动物层次
对象挨个放在GC.calloc
分配的内存块,子类大小不同,展示后面对象位置可由先前大小决定.
这样分配缓冲:
auto capacity = 10_000;
void * buffer = GC.calloc(capacity);//应该合适
确保对对象,有可用容量.
Cat cat = emplace!Cat(catPlace);
// ...
Parrot parrot =emplace!Parrot(parrotPlace, [ "squawk", "arrgh" ]);
注意Parrot
的构造参数在对象地址后指定.
emplace
返回的变量在存储在稍后每一循环使用的动物
切片里面.
Animal[] animals;
// ...
animals ~= cat;
// ...
animals ~= parrot;
foreach (animal; animals) {
writeln(animal.sing());
}
全部:
import std.stdio;
import std.algorithm;
import std.conv;
import core.memory;
// ...
void main() {
Animal[] animals;//动物变量切片
auto capacity = 10_000;//硬编码分配缓冲,假定合适,一般必须验证
void * buffer = GC.calloc(capacity);
void * catCandidateAddr = buffer;
void * catAddr = nextAlignedAddress!Cat(catCandidateAddr);
//先放一个猫
writeln("Cat address : ", catAddr);
size_t catSize = __traits(classInstanceSize, Cat);
//对类对象原位,要求一个`空[]`切片,我们必须先从指针产生切片
void[] catPlace = catAddr[0..catSize];
Cat cat = emplace!Cat(catPlace);
//切片里面构造猫,为稍后用存储返回类变量
animals ~= cat;
void * parrotCandidateAddr = catAddr + catSize;
//在满足对齐要求的下个可用地址里面构造鹦鹉
void * parrotAddr =nextAlignedAddress!Parrot(parrotCandidateAddr);
writeln("Parrot address: ", parrotAddr);
size_t parrotSize = __traits(classInstanceSize, Parrot);
void[] parrotPlace = parrotAddr[0..parrotSize];
Parrot parrot =emplace!Parrot(parrotPlace, [ "squawk", "arrgh" ]);
animals ~= parrot;
foreach (animal; animals) {//用对象
writeln(animal.sing());
}
}
函数模板newObject(T)
比对每个对象重复构造更有用.
显式消灭对象
new
的逆向操作是消灭对象并把内存返回给垃集.一般在未指定时间自动运行.有时需要在指定点析构.当结束对象时,立即执行析构器,而在析构器中可能得关闭文件
对象.
destroy(variable);
调用对象的析构器.
调用析构器后,destroy
置变量为.init
状态,注意类变量的.init
状态为无效(null)
,所以一旦消灭类变量后,就不能用了.destroy
只是执行析构器,由垃集
决定重用对象占用的内存.
警告:当同构指针
用时,destroy
必须接受被指对象,而不是指针
.否则,指针为无效
,而未消灭(析构)对象.
import std.stdio;
struct S {
int i;
this(int i) {
this.i = i;
writefln("用%s值构造", i);
}
~this() {
writefln("用%s值析构", i);
}
}
void main() {
auto p = new S(42);
writeln("destroy()前");
destroy(p);//错误用法
writeln("destroy()后");
writefln("p: %s", p);
writeln("离开主");
}
当destroy
接收指针时,指针->无效
.而不是指针的对象
变为无效.
正确写法是destroy( * p);
最后一行是为相同对象再执行一次析构器,现在其值为S.init,这就是垃集
浪费的地方.
按名在运行时构造对象
Object
的factory
成员函数取类类型的全名
作参数,构造类型对象,并返回那个对象的类变量.
module test_module;
import std.stdio;
interface Animal {
string sing();
}
class Cat : Animal {
string sing() {
return "meow";
}
}
class Dog : Animal {
string sing() {
return "woof";
}
}
void main() {
string[]toConstruct = [ "Cat", "Dog", "Cat" ];
Animal[] animals;
foreach (typeName; toConstruct) {
// __MODULE__伪变量是当前模块名,可在编译时作为串字面量.
const fullName=__MODULE__~'.' ~ typeName;
writefln("构造 %s", fullName);
animals ~= cast(Animal)Object.factory(fullName);
}
foreach (animal; animals) {
writeln(animal.sing());
}
}
虽然无显式new
式,但构造了3个对象,并加至动物切片.
注意Object.factory()
以全名
作参数,factory
返回类型为对象
,在程序使用前,必须转换
为实际类型.
垃集
在未指定时间扫描.确定程序不再使用对象,销毁并回收他们.
程序员可用GC.collect, GC.disable, GC.enable, GC.minimize,
一定程度上控制他们
GC.calloc
和其他函数保留内存,GC.realloc
扩展先前内存,GC.free
将其返回至垃集.
可给分配的内存标记为GC.BlkAttr.NO_SCAN, GC.BlkAttr.NO_INTERIOR
等.
.alignof
是类型的默认对齐,可用classInstanceAlignment
取类对象的默认对齐.
.offsetof
是对象成员与对象开始的偏移字节数.
align
指定变量,用户定义类型,成员
的对齐.
emplace
构造结构时用指针,类对象时用void[]
切片.
destroy
执行对象析构器,你必须消灭构对象
,而不是其指针.
Object.factory()
用全名构造对象.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现