Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add std/smartptrs #17333

Closed
wants to merge 35 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
3273fd7
improve test coverage for isolation
ringabout Mar 8, 2021
b4fa432
a bit better
ringabout Mar 8, 2021
421cf08
Merge remote-tracking branch 'upstream/devel' into a141
ringabout Mar 9, 2021
ea5fe84
Merge remote-tracking branch 'upstream/devel' into a144
ringabout Mar 10, 2021
7b329bc
add std/smartptrs
ringabout Mar 11, 2021
4aae28e
more
ringabout Mar 11, 2021
b6af261
add comments
ringabout Mar 11, 2021
f9bf78d
add comments
ringabout Mar 11, 2021
5e3eb8d
up
ringabout Mar 11, 2021
57e18ad
add comment
ringabout Mar 11, 2021
162db95
Merge remote-tracking branch 'upstream/devel' into a147
ringabout Mar 19, 2021
120716f
Apply suggestions from code review
ringabout Mar 19, 2021
5566515
Merge branch 'a147' of https://github.com/xflywind/Nim into a147
ringabout Mar 19, 2021
6f30a8c
use doAssert
ringabout Mar 19, 2021
7992e60
smartptrs
ringabout Mar 19, 2021
ef0819e
add changelog
ringabout Mar 19, 2021
fe8b805
a bit more tests
ringabout Mar 19, 2021
546cfb9
add examples
ringabout Mar 21, 2021
d59eca6
disable freebsd
ringabout Mar 21, 2021
d62f0e2
Merge branch 'devel' into a147
ringabout Mar 21, 2021
5bbbdc5
disable copy instead of assign
ringabout Apr 5, 2021
8503a75
polish
ringabout Apr 5, 2021
2910418
fix
ringabout Apr 5, 2021
290097c
Apply suggestions from code review
ringabout Apr 5, 2021
9321cc6
Update lib/std/smartptrs.nim
Araq Apr 10, 2021
246bff5
Update lib/std/smartptrs.nim
Araq Apr 10, 2021
df0b217
Update lib/std/smartptrs.nim
Araq Apr 10, 2021
275887c
Update lib/std/smartptrs.nim
Araq Apr 10, 2021
abd5937
Update tests/stdlib/tsmartptrs.nim
ringabout Apr 10, 2021
7a85e22
Update lib/std/smartptrs.nim
ringabout Apr 10, 2021
6b7226a
Update lib/std/smartptrs.nim
ringabout Apr 10, 2021
fd9bdf5
Merge remote-tracking branch 'upstream/devel' into a147
ringabout Apr 11, 2021
56afd30
Merge branch 'a147' of https://github.com/xflywind/Nim into a147
ringabout Apr 11, 2021
9c9f915
better
ringabout Apr 11, 2021
3111c8c
remove deleter
ringabout Apr 20, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,9 @@

- Added `jsconsole.dir`, `jsconsole.dirxml`, `jsconsole.timeStamp`.

- Added `std/smartptrs`, use `-d:nimExperimentalSmartptrs`
to enable this experimental module.

- Added dollar `$` and `len` for `jsre.RegExp`.

- Added `hasDataBuffered` to `asyncnet`.
Expand Down
211 changes: 211 additions & 0 deletions lib/std/smartptrs.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
#
#
# Nim's Runtime Library
# (c) Copyright 2021 Nim contributors
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.

ringabout marked this conversation as resolved.
Show resolved Hide resolved
## C++11 like smart pointers. They always use the shared allocator.
ringabout marked this conversation as resolved.
Show resolved Hide resolved


when defined(nimExperimentalSmartptrs) or defined(nimdoc):
import std/isolation


type
UniquePtr*[T] = object
## Non copyable pointer to a value of type `T` with exclusive ownership.
val: ptr T

proc `=destroy`*[T](p: var UniquePtr[T]) =
if p.val != nil:
`=destroy`(p.val[])
when compileOption("threads"):
deallocShared(p.val)
else:
dealloc(p.val)

proc `=copy`*[T](dest: var UniquePtr[T], src: UniquePtr[T]) {.error.}
## The copy operation is disallowed for `UniquePtr`, it
## can only be moved.

proc newUniquePtr[T](val: sink Isolated[T]): UniquePtr[T] {.nodestroy.} =
## Returns a unique pointer which has exclusive ownership of the value.
when compileOption("threads"):
result.val = cast[ptr T](allocShared(sizeof(T)))
else:
result.val = cast[ptr T](alloc(sizeof(T)))
# thanks to '.nodestroy' we don't have to use allocShared0 here.
# This is compiled into a copyMem operation, no need for a sink
# here either.
result.val[] = val.extract
# no destructor call for 'val: sink T' here either.

template newUniquePtr*[T](val: T): UniquePtr[T] =
Copy link
Member

@timotheecour timotheecour Jul 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as mention in #18450 (comment) for the other PR, this is what's needed:

proc newUniquePtr*[T](U: typedesc[T]): UniquePtr[T] =
  result.val = cast[ptr T](allocShared(sizeof(T)))

which also ends up producing correct results for the C++ example i gave, unlike what the other APIs do.

read the whole thread starting here for more context: #18450 (comment)

I also wonder whether we actually need proc newUniquePtr[T](val: sink Isolated[T]): UniquePtr[T] {.nodestroy.} = given that proc newUniquePtr*[T](U: typedesc[T]): UniquePtr[T] = seems simpler, more correct, and more performant (no copy)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand. Don't you want the unique pointer to point to some valid object? Your proc doesn't do the same.

Copy link
Member

@timotheecour timotheecour Jul 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm allocating on the heap only once instead of allocating in caller scope (eg on stack) and then allocating on heap and copying; it's more efficient and analogous to C++ placement new on which C++ make_unique relies, avoids complications of isolated, and is also more correct wrt destructors being called as you can see in #18450 (comment)

Caller can then, if needed, assign values in the heap allocated object, eg:

import std/smartptrs

type ManagedFile = object
  name: string
  cfile: File

proc `=destroy`(a: var ManagedFile) =
  echo ("in =destroy", a.name)
  close(a.cfile)
  a.cfile = nil

proc main()=
  block:
    let p = newUniquePtr(ManagedFile)
    p[].name = currentSourcePath
    p[].cfile = open(p[].name, fmRead)
    echo p[].cfile.readAll.len
  echo "after"
main()

prints:

381
("in =destroy", "/Users/timothee/git_clone/nim/timn/tests/nim/all/t12621.nim")
after

It's also the correct way to wrap C++ objects (eg with non-trivial constructor): it allows the C++ object to be constructed exactly once, and then destroyed when the unique_ptr goes out of scope.

Copy link
Contributor

@planetis-m planetis-m Jul 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@timotheecour its already taken care of https://github.com/nim-lang/threading/blob/master/threading/smartptrs.nim#L48 Its not more efficient though, you should use the sink one (unless you have to) as I said in #17333 (comment)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    p[].name = currentSourcePath
    p[].cfile = open(p[].name, fmRead)

is not as efficient as direct inplace construction. Not because of the lack of moves, but because the compiler doesn't know that in p[] there is nothing to destroy yet.

Copy link
Member

@Araq Araq Jul 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's one, showing that the way I proposed is 3.4X faster, where the speedup depends on the size of the object; you'll get smaller speedups for smaller objects but always at least as fast.

But that's measuring artifacts of the current implementation, in particular NVRO (which is unfortunately currently fragile) or the lack thereof. In theory it's a pessimization as you mitigated the compiler's ability to reason about the code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still don't understand the reasoning here; in current code, you:
1 construct an object Foo in the stack
2 allocate sizeof(Foo) on the heap
3 copy Foo

with proposed code you:
1 allocate sizeof(Foo) on the heap
2 construct in-place

in all possible scenarios, it should be at least as fast, and usually faster; eg it allows placement-new with C++, or simply just constructing in-place the fields you care about updating from their default value (if some of the memory is to remain 0-initialized, you don't pay for it); you also don't have extra ctor/dtor to worry about, in particular for C++ objects.

n element version

Finally, this generalizes in a straightforward way to C++'s array version of make_unique, which we should add at some point; it allows dynamic allocation of n objects without having to use a seq (eg so you can interface with C++ or avoid relying on gc for allocating n elements); there are 2 approaches to consider:

  • same as C++, make the n implicit (user code need to pass that value around somewhere else)
proc newUniquePtr*[T](U: typedesc[T], n = 1): UniquePtr[T] =
  result.val = cast[ptr T](allocShared(sizeof(T) * n))
  • add a UniquePtrN type with an extra field n: int
type
  UniquePtrN*[T] = object
    val: ptr T
    n: int
proc newUniquePtrN*[T](U: typedesc[T], n = 1): UniquePtrN[T] =
  result.val = cast[ptr T](allocShared(sizeof(T) * n))
  result.n = n

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow, the loophole is already there,

I am just pointing it out. I had totally missed it before.

n element version

Why not use a seq instead?

Copy link
Member

@Araq Araq Jul 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still don't understand the reasoning here; in current code, you: ...

Your're correct, however, the code is still not as efficient as it could be if we had proper "in-place" construction. I think we could do proper in-place construction for the construct toSinkParam ObjConstr(x: x, y: y, ...).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for c++ objects, it should already be possible today without a language extension (via importcpp of new(buf) T), although i haven't tried yet
for nim objects, it shouldn't be hard to extend semObjConstr to take an optional input buffer address, eg:

var p: ptr Foo = ...
Foo.new(p, a1: 1, a2: 2) # explicit syntax
p[] = Foo(a1: 1, a2: 2) # syntax sugar possible?

in any case, these can be added later, and proc newUniquePtrN*[T](U: typedesc[T]): UniquePtrN[T] = can be introduced before those exist; code would just get auto-optimized once p[] = Foo(a1: 1, a2: 2) starts working as in-place construction

## Returns a unique pointer which has exclusive ownership of the value.
newUniquePtr(isolate(val))

proc get*[T](p: UniquePtr[T]): var T {.inline.} =
## Returns a mutable view of the internal value of `p`.
when compileOption("boundChecks"):
doAssert(p.val != nil, "dereferencing a nil unique pointer")
p.val[]

proc isNil*[T](p: UniquePtr[T]): bool {.inline.} =
p.val == nil

proc `[]`*[T](p: UniquePtr[T]): var T {.inline.} =
p.get

proc `$`*[T](p: UniquePtr[T]): string {.inline.} =
if p.val == nil: "(nil)"
else: "(" & $p.val[] & ")"

#------------------------------------------------------------------------------

type
SharedPtr*[T] = object
## Shared ownership reference counting pointer.
val: ptr tuple[value: T, atomicCounter: int]

proc `=destroy`*[T](p: var SharedPtr[T]) =
if p.val != nil:
if (when compileOption("threads"):
atomicLoadN(addr p.val[].atomicCounter, ATOMIC_CONSUME) == 0 else:
p.val[].atomicCounter == 0):
`=destroy`(p.val[])
when compileOption("threads"):
deallocShared(p.val)
else:
dealloc(p.val)
else:
when compileOption("threads"):
discard atomicDec(p.val[].atomicCounter)
else:
dec(p.val[].atomicCounter)

proc `=copy`*[T](dest: var SharedPtr[T], src: SharedPtr[T]) =
if src.val != nil:
when compileOption("threads"):
discard atomicInc(src.val[].atomicCounter)
else:
inc(src.val[].atomicCounter)
if dest.val != nil:
`=destroy`(dest)
dest.val = src.val

proc newSharedPtr[T](val: sink Isolated[T]): SharedPtr[T] {.nodestroy.} =
## Returns a shared pointer which shares
## ownership of the object by reference counting.
when compileOption("threads"):
result.val = cast[typeof(result.val)](allocShared(sizeof(result.val[])))
else:
result.val = cast[typeof(result.val)](alloc(sizeof(result.val[])))
result.val.atomicCounter = 0
result.val.value = val.extract

template newSharedPtr*[T](val: T): SharedPtr[T] =
## Returns a shared pointer which shares
## ownership of the object by reference counting.
newSharedPtr(isolate(val))

proc get*[T](p: SharedPtr[T]): var T {.inline.} =
## Returns a mutable view of the internal value of `p`.
when compileOption("boundChecks"):
doAssert(p.val != nil, "dereferencing a nil shared pointer")
p.val.value

proc isNil*[T](p: SharedPtr[T]): bool {.inline.} =
p.val == nil

proc `[]`*[T](p: SharedPtr[T]): var T {.inline.} =
p.get

proc `$`*[T](p: SharedPtr[T]): string {.inline.} =
if p.val == nil: "(nil)"
else: "(" & $p.val.value & ")"

#------------------------------------------------------------------------------

type
ConstPtr*[T] = distinct SharedPtr[T]
## Distinct version of `SharedPtr[T]`, which doesn't allow mutating the underlying value.

template newConstPtr*[T](val: T): ConstPtr[T] =
## Similar to `newSharedPtr<#newSharedPtr,T>`_, but the underlying value can't be mutated.
ConstPtr[T](newSharedPtr(isolate(val)))

proc get*[T](p: ConstPtr[T]): lent T {.inline.} =
## Returns an immutable view of the internal value of `p`.
when compileOption("boundChecks"):
doAssert(SharedPtr[T](p).val != nil, "dereferencing nil const pointer")
SharedPtr[T](p).val.value

proc isNil*[T](p: ConstPtr[T]): bool {.inline.} =
SharedPtr[T](p).val == nil

proc `[]`*[T](p: ConstPtr[T]): lent T {.inline.} =
p.get

proc `$`*[T](p: ConstPtr[T]): string {.inline.} =
if SharedPtr[T](p).val == nil: "(nil)"
else: "(" & $SharedPtr[T](p).val.value & ")"


runnableExamples("-d:nimExperimentalSmartptrs"):
import std/isolation

block:
var a1: UniquePtr[float]
var a2 = newUniquePtr(0)

assert $a1 == "(nil)"
assert a1.isNil
assert $a2 == "(0)"
assert not a2.isNil
assert a2[] == 0
assert a2.get == 0

# UniquePtr can't be copied but can be moved
let a3 = move a2 # a2 will be destroyed

assert $a2 == "(nil)"
assert a2.isNil

assert $a3 == "(0)"
assert not a3.isNil
assert a3[] == 0
assert a3.get == 0

block:
var a1: SharedPtr[float]
let a2 = newSharedPtr(0)
let a3 = a2

assert $a1 == "(nil)"
assert a1.isNil
assert $a2 == "(0)"
assert not a2.isNil
assert a2[] == 0
assert a2.get == 0
assert $a3 == "(0)"
assert not a3.isNil
assert a3[] == 0
assert a3.get == 0

block:
var a1: ConstPtr[float]
let a2 = newConstPtr(0)
let a3 = a2

assert $a1 == "(nil)"
assert a1.isNil
assert $a2 == "(0)"
assert not a2.isNil
assert a2[] == 0
assert a2.get == 0
assert $a3 == "(0)"
assert not a3.isNil
assert a3[] == 0
assert a3.get == 0
86 changes: 86 additions & 0 deletions tests/stdlib/tsmartptrs.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
discard """
targets: "c cpp"
disabled: "freebsd"
matrix: "--gc:refc -d:nimExperimentalSmartptrs; --gc:orc -d:nimExperimentalSmartptrs"
"""


import std/[unittest, smartptrs, isolation]

block: # UniquePtr[T] test
var a1: UniquePtr[float]
var a2 = newUniquePtr(0)

check:
$a1 == "(nil)"
a1.isNil
$a2 == "(0)"
not a2.isNil
a2[] == 0

let a3 = move a2

check:
$a2 == "(nil)"
a2.isNil

$a3 == "(0)"
not a3.isNil
a3[] == 0

block: # SharedPtr[T] test
var a1: SharedPtr[float]
let a2 = newSharedPtr(0)
let a3 = a2
check:
$a1 == "(nil)"
a1.isNil
$a2 == "(0)"
not a2.isNil
a2[] == 0
$a3 == "(0)"
not a3.isNil
a3[] == 0

block: # ConstPtr[T] test
var a1: ConstPtr[float]
let a2 = newConstPtr(0)
let a3 = a2

check:
$a1 == "(nil)"
a1.isNil
$a2 == "(0)"
not a2.isNil
a2[] == 0
$a3 == "(0)"
not a3.isNil
a3[] == 0

block: # UniquePtr[T] test
var a1 = newUniquePtr("1234")

proc hello(x: string) =
doAssert x == "1234"

hello(a1.get)

block: # SharedPtr[T] test
let x = 5.0
let a1 = newSharedPtr(x)
let a2 = a1

proc hello(x: float) =
doAssert x == 5.0

hello(a2.get)

block: # SharedPtr[T] test
let x = 5.0
let a1 = newConstPtr(x)
let a2 = a1

proc hello(x: float) =
doAssert x == 5.0

hello(a2.get)