Office and cleaning in D
We all know that D uses the garbage collector. He controls the allocation of memory. Implementations of such built-in types as associative and dynamic arrays, strings (which are also arrays), exceptions, and delegates use it. Also, its use is embedded in the syntax of the language (concatenation, new operator). GC removes responsibility and burden from the programmer, allows you to write more compact, understandable and safe code. And these are perhaps the most important pluses of the garbage collector. Is it worth it to refuse? Paying for the use of the collector will be excessive memory consumption, which is unacceptable with very limited resources and the pause of all threads (stop-the-world) on the assembly itself. If these points are critical for you, welcome to cat.
How bad is it?
The first thing you need to find out, but is it bad at all?
You can use valgrind , its memcheck tool (by default) will show how many times the program allocated and freed memory, as well as its amount (total heap usage line).
But valgrind will not be able to show GC usage statistics. Fortunately, this is built into runtime D (dmd only). The garbage collector of an already compiled program can be configured and profiled as follows:
app "--DRT-gcopt=profile:1 minPoolSize:16" program args
The first argument (string) is processed by runtime and does not reach the main function.
Supported options:
- disable: 0 | 1 - disable the collector
- profile: 0 | 1 - profiling with output at the end
- initReserve: N - reserved at startup memory (Mb)
- minPoolSize: N - initial and minimum pool size (Mb)
- maxPoolSize: N - maximum pool size (MB)
- incPoolSize: N - step to increase the pool (MB)
- heapSizeFactor: N - ratio of target heap size to used memory
When profiling is enabled, the output of the program after its completion will be something like this:
Number of collections: 101
Total GC prep time: 10 milliseconds
Total mark time: 3 milliseconds
Total sweep time: 3 milliseconds
Total page recovery time: 0 milliseconds
Max Pause Time: 0 milliseconds
Grand total GC time: 17 milliseconds
GC summary: 67 MB, 101 GC 17 ms, Pauses 13 ms < 0 ms
Life without assembly (almost)
If after all the tests and adjusting the GC the result remains unsatisfactory, you can resort to some tricks.
No collector needed - don't use
Seriously? Is that allowed?
In critical sections of the program, the collector can simply be disabled:
import core.memory;
...
GC.disable();
...
And when “there will be time for cleaning”, turn it back on or immediately start:
...
GC.enable();
GC.collect(); // enable перед collect делать не обязательно, он сам включится
...
At the time of completion, the program starts the garbage collector again, regardless of the state of its inclusion.
When using this technique, it is important to remember that memory continues to be allocated in the event that it is not enough, the program will be completed by the OS.
Use the correct types
As already mentioned at the beginning of this article, arrays, classes, and delegates are not the most suitable candidates for use when trying to get away from GC.
Some classes can be replaced with structures. In D, structures are allocated on the stack and destroyed when leaving the scope. If there are no classes without classes, then you can use it only in scope:
import std.typecons;
...
auto cls = scoped!MyClass( param, of, my, _class );
...
The cls object will behave as an instance of the MyClass class, but will be destroyed when it leaves the scope without the participation of GC. It is worth replacing that the scope keyword for creating class objects is to be used in favor of a library implementation, here is a discussion.
Ranges!
A separate round and, as I understand it, the current development trend of the standard library is a transition to the range of ranges. So now almost all the functions from std.algorithm work. Ranges can be different: input, output, infinite, with length, etc.
Their meaning is that they are objects (structures) containing certain methods, such as front, popFront, etc. Learn more about which structures can act as ranges in the standard library . Their advantages are delayed computing and no memory allocation. A simple example:
import std.stdio;
import std.typetuple;
import std.range;
import std.array;
template isIRWL(R) { enum isIRWL = isInputRange!R && hasLength!R; }
template FloatHandler(R) { enum FloatHandler = is( ElementType!R == float ); }
float avg(R1,R2)( R1 a, R2 b )
if( allSatisfy!(isIRWL,R1,R2) && allSatisfy!(FloatHandler,R1,R2) )
{
auto c = chain( a, b ); // соединяем в один диапазон
float res = 0.0f;
foreach( val; c ) res += val; // foreach одобряет)
return res / c.length; // не все InputRange имеют длину
}
void main()
{
float[] a = [1,2,3];
float[] b = [4,5,6,7];
writeln( avg( a, b ) ); // 4
float[] d = chain( a, b ).array; // легко сделать массив
writeln( d ); // [1,2,3,4,5,6,7]
}
The chain function returns an object of type Result (local to the function), which contains 2 references to the ranges that were specified at the input. When enumerating this object with foreach, the front and popFront methods are called, and this object calls the corresponding methods first in the first range, then in the second, when the first becomes empty.
A good presentation on the subject of ranges was at DConf2015 by Jonathan M Davis.
If you really want classes
Yes, those that are constantly being created and deleted. In this case, you can redefine the class a bit and use the concept of FreeList
class Foo
{
static Foo freelist; // голова списка
Foo next; // используется для реализации списка
static Foo allocate()
{
Foo f;
if( freelist ) // если у нас есть свободный объект
{
f = freelist; // берём его
freelist = f.next;
}
else f = new Foo(); // иначе создаём новый
return f;
}
static void deallocate(Foo f) // ненужный объект добавляем в список свободных
{
f.next = freelist;
freelist = f;
}
... тут основные методы класса ...
}
...
Foo f = Foo.allocate();
...
Foo.deallocate(f);
In this case, we will minimize the memory allocation for new objects, if such have already been created and are no longer needed. This does not completely block us from the collector, but if memory is not allocated, then the collector will not start the assembly.
Life without assembly (well, if only a little)
I did not find a way to write fully in D without using the compiler, but this is partly, in my opinion, good. Manual memory management is fraught with errors, unsafe, cumbersome, etc. (old and evil C ++). But if you really need it, then you can.
Functions from libc malloc and free are used for manual memory management. To work with arrays, this is elementary:
import core.stdc.stdlib;
...
auto arr = (cast(float*)malloc(float.sizeof*count))[0..count];
...
free( arr.ptr );
...
You can use the @nogc attribute to protect yourself from unwanted use of GC. The compiler will throw an error when it detects the use of the collector inside blocks with such an attribute.
void foo() {}
void func(int[] arr) @nogc
{
auto a = new MyClass; // ошибка
arr ~= 42; // ошибка
foo(); // ошибка: вызов функции, не помеченной как @nogc
}
To maintain flexibility of use, do not specify attributes to template functions. If the template function will be called from @nogc code, the compiler will try to make it also @nogc. To do this, the condition must be maintained that only @nogc functions are used inside this template function. This compiler behavior is useful in the case of reusing template code, when the template function will be needed when using the collector (it will be called from regular code and will use regular code inside itself). This also applies to other attributes (nothrow, pure, etc).
When compiling, you can display all the places in the program where the collector is used:
dmd -vgc source.d ...
The compiler will only point to places of use, but will not generate an error.
It must be remembered that when creating streams through the standard library, a collector is also used. To create threads without a collector, you must use C functions, as in the case of malloc and free.
And lastly: creating classes without a collector
A small example with comments
import std.stdio;
import core.exception;
import core.stdc.stdlib : malloc, free;
import core.stdc.string : memcpy;
import core.memory : GC;
import std.traits;
class A
{
int x;
this( int X ) { x = X; }
int foo() { return 2 * x; }
}
class B : A
{
int z = 2;
this( int x ) { super(x); }
override int foo() { return 3 * x * z; }
}
// std.conv.emplace не сможет быть @nogc, поэтому переписан
T classEmplace(T,Args...)( void[] chunk, auto ref Args args )
if( is(T == class) )
{
enum size = __traits(classInstanceSize, T); // узнаём размер экземпляра класса
// проверяем память, куда будем записывать
if( chunk.length < size ) return null;
if( chunk.length % classInstanceAlignment!T != 0 ) return null;
// объект TypeInfo хранит инициализирующее состояние класса в свойстве init, копируем его в память
// кажется там только виртуальная таблица функций и статические поля класса, могу ошибаться
memcpy( chunk.ptr, typeid(T).init.ptr, size );
auto res = cast(T)chunk.ptr;
// вызываем конструктор
static if( is(typeof(res.__ctor(args))) )
res.__ctor(args);
else
static assert(args.length == 0 && !is(typeof(&T.__ctor)),
"Don't know how to initialize an object of type "
~ T.stringof ~ " with arguments " ~ Args.stringof);
return res;
}
auto heapAlloc(T,Args...)( Args args )
{
enum size = __traits(classInstanceSize, T);
auto mem = malloc(size)[0..size];
if( !mem ) onOutOfMemoryError();
//GC.addRange( mem.ptr, size ); // об этом ниже
return classEmplace!(T)( mem, args );
}
auto heapFree(T)( T obj )
{
destroy(obj);
//GC.removeRange( cast(void*)obj ); // и об этом тоже
free( cast(void*)obj );
}
void main()
{
auto test = heapAlloc!B( 12 );
writeln( "test.foo(): ", test.foo() ); // 72
heapFree(test);
}
As for the commented out lines of GC.addRange () and GC.removeRange (). If you have firmly decided that you will not use the collector, you can leave them commented out. If arrays, delegates, other classes, etc. are to be stored inside the class, which must be removed using the GC, then you need to add a range of memory to the GC that it will scan to find garbage.
If the constructor is @nogc, then you can use heapAlloc in the @nogc function, with heapFree everything is more complicated: destroy, in addition to calling the destructors (which can be implemented quite simply with a mixin), also performs some actions related to the class monitor (of course, if you want, you can and replace them with the @nogc option).
Conclusion
In the development of the language and the standard library, one can observe a tendency to abandon the "forced" use of the garbage collector. At the moment, work on this is far from complete, but there is some progress.
In this regard, the reports of Walter Bright and Andrei Alexandrescu from the same DConf2015 seemed interesting to me .
PS. Why on a habr there is no syntax highlighting D yet?
PPS Does anyone know if D conferences are planned in the Russian Federation?