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

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