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,这就是垃集浪费的地方.
按名在运行时构造对象
Objectfactory成员函数取类类型的全名作参数,构造类型对象,并返回那个对象的类变量.

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()用全名构造对象.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章