One of the interesting things I remember about Nim is that you can have ptr object rather than ref object, since ptr object is not garbage collected. How do you create and destroy an ptr object? For example how do you correctly instantiate and destroy the two types below?
type
MyTypeA = ptr object of RootObj
x: int64
MyTypeB = ptr object of RootObj
x: array[0..4, int64]
That is also covered in my book in detail. Have you starting reading?
http://ssalewski.de/nimprogramming.html#_allocating_objects
alloc(), create(), dealloc().
I just wasn't sure whether when allocating for objects you have to account for any "gubbings", just cautious I guess:
type
MyTypeA = ptr object of RootObj
x: int64
MyTypeB = ptr object of RootObj
x: array[0..4, int64]
var x0 = alloc(sizeof(int64));
var x1 = cast[MyTypeA](x0);
x1.x = 42;
echo "x: ", x1.x;
dealloc(x1); # or dealloc(x0)
var y0 = alloc(sizeof(int64)*5);
var y1 = cast[MyTypeB](y0);
y1.x = [1'i64, 2, 3, 4, 5];
echo "y1: ", y1.x;
dealloc(y1); # or dealloc(y0)
I've now added your book to my Nim reading list. Thanks
you can't mix inheritance with un-managed raw pointers
That is an interesting point, I was wondering about it this morning already when I wrote my first reply.
I am still not sure. I could generally imagine that inheritance can work with with un-managed raw pointers. But maybe not in Nim?
@dataPulverizer note that create() may be easier to use, as with create() no cast is necessary.
@snej The book is in a very early stage still, I wrote the two chapters in April. And it is mostly for people with very less programming experience.
you can't mix inheritance with un-managed raw pointers.
I can do it. In fact, I did in Nim's allocator.
The allure of undocumented behaviors ;)
@dataPulverizer, if you already use pointers you can use generic procs, why do you need inheritance for dispatch?
One of the things that intrigued me about Nim was it's object system, you could create value, reference and pointer types with/without inheritance. As I understand it, an important distinction between ptr and ref classes is that ref classes are managed by the garbage collector while ptr classes have to be manually allocated and deallocated.
In this case for me that is the distinction between both of them, I see inheritance from a ptr class as the same as inheritance from a ref class and polymorphism in the same way:
type:
Animal = ptr object of RootObj
Cat = ptr object of Animal
var x: Animal = create(Cat)
But I also want to be able to dispatch against Cat (x) at runtime, so send it as a parameter in a function marked with Animal but within that function call the cat method and allow the right method to be dispatched. In exactly the same way as in my kernel function:
proc calculateKernelMatrix*(K: AbstractKernel, data: Matrix[F]): Matrix[F] =
let n = int64(ncol(data));
var mat = Matrix[F](data: newSeq[F](n*n), dim: @[n, n]);
for j in 0..<n:
for i in j..<n:
var tmp: F;
mat[i, j] = kernel(K, data.col(i), data.col(j));
mat[j, i] = mat[i, j];
return mat;
It doesn't matter that AbstractKernel is a ref or ptr type, without polymorphism I can't runtime dispatch the right method at the kernel function. This can not happen without inheritance and polymorphism, at runtime overriding the parent method with the child method while providing the same interface in the usual way.
At compile time yes you could just use procs but you need polymorphism for runtime dispatch.
No you don't, you can have AbstractKernel be a procNo you don't, you can have AbstractKernel be a proc.
type AbstractKernel[F] = proc(loc: var F) {.nimcall.}
I don't understand and have a couple of questions. Firstly, what would be the syntax for other kernel functions that inherit? Secondly, the DotKernel function is the only function whose type doesn't carry data, the vast majority of the other kernel functions do, for example:
Gaussian*[T] = ref object of AbstractKernel
gamma: T
Other kernels have different number (and names) of data members so how would you represent that with your function types?
Thanks
You can use closures or pass the data parameter as an input to the function.
Here is an example with closures
type
AbstractKernel[T] = proc(dst: var T, src: Matrix[T], i, j: int) {.closure.}
Matrix[T] = object
dim: array[2, int]
data: seq[T]
func newMatrix(rows, cols: int, T: typedesc): Matrix[T] =
result.dim[0] = rows
result.dim[1] = cols
result.data.newSeq(rows * cols)
func ncols(m: Matrix): int {.inline.} =
m.dim[1]
func nrows(m: Matrix): int {.inline.} =
m.dim[0]
template `[]`(m: Matrix, i, j: int): auto =
m.data[i * m.dim[1] + j]
template `[]=`[T](m: var Matrix[T], i, j: int, val: T) =
m.data[i * m.dim[1] + j] = val
proc calculateKernelMatrix*[T](m: Matrix[T], kernel: AbstractKernel[T]): Matrix[T] =
result = newMatrix(m.nrows, m.ncols, T);
for i in 0 ..< m.nrows:
for j in 0 ..< m.ncols:
kernel(result[i, j], m, i, j);
let M = newMatrix(3, 3, int)
echo M # (dim: [3, 3], data: @[0, 0, 0, 0, 0, 0, 0, 0, 0])
let N = M.calculateKernelMatrix do (dst: var int, src: Matrix[int], i, j: int):
let state = 100
dst = state * i + j
echo N # (dim: [3, 3], data: @[0, 1, 2, 100, 101, 102, 200, 201, 202])
let K = N.calculateKernelMatrix do (dst: var int, src: Matrix[int], i, j: int):
let state = -1
dst = src[i, j] * state
echo K # (dim: [3, 3], data: @[0, -1, -2, -100, -101, -102, -200, -201, -202])
state can be anything beyond a simple int.
For your example ARC makes a gigantic difference. (I put your global code in a main proc)
import times
type FooBase = ref object {.inheritable.}
dummy: int
type Foo{.final.} = ref object of FooBase
value : float32
proc inplace_add_proc(x: var Foo, a: float32) =
x.value += a
proc inplace_add_closure(x: var float32, a: float32) =
proc add_closure(v: var float32) = v += a
add_closure(x)
method inplace_add_method(x: FooBase, a: float32) {.base.} =
discard
method inplace_add_method(x: Foo, a: float32) =
x.value += a
proc main =
var bar : Foo
new bar
var start = cpuTime()
for i in 0..<100000000:
inplace_add_proc(bar, 1.0f)
echo " Proc with ref object ", cpuTime() - start
var x : float32
start = cpuTime()
for i in 0..<100000000:
inplace_add_closure(x, 1.0f)
echo " Closures ", cpuTime() - start
var baz : Foo
new baz
start = cpuTime()
for i in 0..<100000000:
inplace_add_method(baz, 1.0f)
echo " Methods ", cpuTime() - start
main()
$ nim c -r -d:danger t3.nim
Proc with ref object 0.117909274
Closures 1.719076293
Methods 0.2647902979999999
$ nim c -r -d:danger --gc:arc t3.nim
Proc with ref object 0.118106775
Closures 0.9414397170000001
Methods 3.822302076
@mratsim Thank you for your detailed and informative response, also I didn't know about the do statement in Nim that's something else new for me. My main takeaway is that I don't always have to use runtime resolution, especially because the kind of code I write is mostly statistics/machine learning - type algorithms where performance is important and virtual method calls (dynamic dispatch) has an adverse impact.
To be clear the example you gave using AbstractKernel[T] is resolved at compile time rather than at run time, AbstractKernel[T] is essentially a function pointer whose type (along with the matrix) is known at compile time. I guess OOP-based polymorphism is one way to provide a common interface to function dispatch and parametric polymorphism is another - which has no performance penalties but can not be used in instances where type resolution at compile time is not available, but in instances where this is not the case it pays to use parametric polymorphism.
For machine learning in particular deep learning this is how I do it:
1. I use inheritance, but only to be able to store the objects in the same container, never for dispatch. Example on neural network layer, called Gate, re-using the terminology from Andrej Karpathy's Hacker's Guide to Neural Network
https://github.com/mratsim/Arraymancer/blob/5b24877b/src/autograd/autograd_common.nim#L72-L85
type
Gate*[TT] = ref object of RootObj # {.acyclic.}
## Base operator or layer. You can describe your custom operations or layers
## by inheriting from Gate and add a forward and optionally a backward method.
## Each operations should set the number of gradients produced during backpropagation.
## Additional fields specific to the operations like weights or inputs cache should be added too.
PayloadKind* = enum
pkVar, pkSeq
Payload*[TT] = object
case kind*: PayloadKind
of pkVar: variable*: Variable[TT]
of pkSeq: sequence*: seq[Variable[TT]]
Backward*[TT] = proc(self: Gate[TT], payload: Payload[TT]): SmallDiffs[TT] {.nimcall.}
2. For dispatching I associate a compile-time proc for each kind of Gate I have. The Gate carry the state I need to pass. The compile-time proc just unwrap it and pass it to the _real which has the proper signature.
For example for a MaxPool layer, the backward proc should have for signature. https://github.com/mratsim/Arraymancer/blob/5b24877b/src/nn_primitives/nnp_maxpooling.nim#L70-L74 So how to map it to proc(self: Gate[TT], payload: Payload[TT]): SmallDiffs[TT] {.nimcall.}?
The MaxPoolGate and backward shim does the trick https://github.com/mratsim/Arraymancer/blob/5b24877b/src/nn/layers/maxpool2D.nim#L20-L46, maxpool2D_backward_ag stands for autograd version
type MaxPool2DGate*[TT] {.final.} = ref object of Gate[TT]
cached_input_shape: MetadataArray
cached_max_indices: Tensor[int]
kernel, padding, stride: Size2D
proc maxpool2D_backward_ag[TT](self: MaxPool2DGate[TT], payload: Payload[TT]): SmallDiffs[TT] =
let gradient = payload.variable.grad
result = newDiffs[TT](1)
result[0] = maxpool2d_backward(
self.cached_input_shape,
self.cached_max_indices,
gradient
)
So I get compile-time dispatch on arbitrary state with a fixed interface. And even though the wrapper proc does not much, 2 static function calls are always faster than a closure or a method.
Other example, convolution, this may seem more complex as the backward operation has a lot of inputs: https://github.com/mratsim/Arraymancer/blob/5b24877b/src/nn_primitives/nnp_convolution.nim#L65-L70
proc conv2d_backward*[T](input, weight, bias: Tensor[T],
padding: Size2D,
stride: Size2D,
grad_output: Tensor[T],
grad_input, grad_weight, grad_bias: var Tensor[T],
algorithm = Conv2DAlgorithm.Im2ColGEMM)
But it's actually straightforward as well
type Conv2DGate*[TT]{.final.} = ref object of Gate[TT]
cached_input: Variable[TT]
weight, bias: Variable[TT]
padding, stride: Size2D
# TODO: store the algorithm (NNPACK / im2col)
proc conv2d_backward_ag[TT](self: Conv2DGate[TT], payload: Payload[TT]): SmallDiffs[TT] =
let gradient = payload.variable.grad
if self.bias.isNil:
result = newDiffs[TT](2)
else:
result = newDiffs[TT](3)
conv2d_backward(
self.cached_input.value,
self.weight.value, self.bias.value,
self.padding, self.stride,
gradient,
result[0], result[1], result[2]
)
2 Stefan_Salewski - is your book published or it is "in process" state? I saw some referred (in the text) mistypings like:
var
mean = 3.0 / 7.9
x: float = 12
y: 1.2E3
For your example ARC makes a gigantic difference.
Yeah, known problem. Method dispatch got super slow for ARC. Not sure yet how to fix it. I can do it the old way without problems, but where is the fun in that. ;-)
y: 1.2E3
Thanks for that hint. Of course the book is in an very early state still, I wrote most of the two first chapters in April. But I hope and think that it can be already useful for beginners with nearly no CS knowledge. I have just yesterday applied a lot fixes provided by Jim Wilcoxson, mostly concerning English grammar. Current book content is the result of a fast flow directly from my head to the keyboard, so examples are untested and book content is not compared to the Nim manual in detail still. For reporting small issues you may use github at https://github.com/StefanSalewski/NimProgrammingBook, or for a general discussion https://forum.nim-lang.org/t/6170#38482.
You can use closures or pass the data parameter as an input to the function.
In some sense closures and objects are isomorphic, as are functional and OOP. There’s a famous old koan about this:
The venerable master Qc Na was walking with his student, Anton. Hoping to prompt the master into a discussion, Anton said “Master, I have heard that objects are a very good thing — is this true?” Qc Na looked pityingly at his student and replied, “Foolish pupil — objects are merely a poor man’s closures.” (continues...)
—Jens