Hi Simon (Šimon?),> You are re-inventing the reference classes (or R6 if you prefer package space). By definition, environments are the only mutable objects in R, no other objects are, so what you are doing is adding a reference wrapper around an immutable object.
yes, what I am doing is essentially very barebone RC or R6, but in pure base R in fewer lines of code.
I stole it from R6 benchmarks: https://r6.r-lib.org/articles/Performance.html#environment-created-by-a-function-call-without-class-attribute
and from https://github.com/r-lib/testthat/blob/main/R/stack.RIn the actual code, I am using an environment as a hidden state object, specifically to track test results:
https://github.com/J-Moravec/mutr/blob/master/mutr.r#L6-L13 so I need an environment object (or bunch of global variables).> Clearly, for the example above the entire discussion is irrelevant, since copies are much cheaper than function calls so unless you have a stack of billions it makes no difference. In addition, your get() will always return a copy regardless, because you are forcing it by the subsetting, so, paradoxically, the naïve (yet more readable) implementation
I guess I was so scared of the Second Circle that I fell in the Eight. https://www.burns-stat.com/pages/Tutor/R_inferno.pdfI didn't realized that the "get" will force a copy, and after testing, the performance is better for growing variant (I guess since R is now internally pre-allocating for short vectors, but as you said, internal details :) ).
I guess I can keep it simple then and don't need to be as worried about re-allocating small vectors.
https://gist.github.com/J-Moravec/07bde03068ece71495976b0388c4b519 Thanks, this was nice learning experience, -- Jirka On 7/01/26 11:12, Simon Urbanek wrote:
On 7/01/2026, at 09:29, Jiří Moravec <[email protected]> wrote: Hi Ivan, can't say that I fully understand yet the described mechanism, namely given what you have described at the end, something I found myself with: env = parent.env() # doesn't work with emptyenv() env$vec = c("a","b") .Internal(address(env$vec)) env2 = env with(env, {vec[1] = "foo"}) Where `with` runs eval(substitute())` internally. --- I am just playing with fixed buffers or stacks, I previously was able to do stack with: new_stack = function(){ size = 0 items = vector("character", 8) add = function(x){ size <<- size + 1 items[size] <<- x } get = function(){ items[seq_len(size)] } environment() } stack = new_stack() tracemem(stack$items) stack2 = stack .Internal(address(stack$items)) stack$add("foo") stack$add("bar") # Memory is the same .Internal(address(stack$items)) # stack2 is the same as stack stack2$get() # [1] "foo" "bar" Which works, is really cool, and allows memory efficient (or so I hope) shared resources with reference-like schematic with other type that environments. I just hoped that further simplification would be possible.You are re-inventing the reference classes (or R6 if you prefer package space). By definition, environments are the only mutable objects in R, no other objects are, so what you are doing is adding a reference wrapper around an immutable object. The fact that `items` can be modified in place is orthogonal to that: as Ivan said, that's just an under-the-hood optimization. The language definition says that the `items` before and after the subassignment are two different objects - both immutable. From user's perspective there is no difference, but R is smart enough to optimize away the copy if it is safe, i.e., when it knows for sure that no one can access the original object so it can cheat and re-use the original object instead, but that fact is intended to be entirely invisible to the user. Clearly, for the example above the entire discussion is irrelevant, since copies are much cheaper than function calls so unless you have a stack of billions it makes no difference. In addition, your get() will always return a copy regardless, because you are forcing it by the subsetting, so, paradoxically, the naïve (yet more readable) implementation items = character() add = function(x) items <<- c(items, x) get = function() items is actually faster if you have comparable number of gets and adds since gets don't need to copy (and uses less memory). My recommendation would be to not worry about internal optimisations because a) they change all the time so your assumptions about undefined behavior may be broken and backfire, b) the time you spend on it is many orders of magnitude more than any potential savings and c) trying to exploit specific behavior makes code less readable and thus more error-prone. Cheers, ŠimonI believe this works because the function "add" is evaluated in the same environment (a with the `with`), but I don't fully get _why_. I will spend some time reading the subset assignment section. On 6/01/26 23:39, Ivan Krylov wrote:В Mon, 5 Jan 2026 16:30:43 +1300 Jiří Moravec <[email protected]> пишет:1. Is there documentation of `reference counting`?There is a short description at <https://developer.r-project.org/Refcnt.html>. The general rule for package developers is "Except in very special and well understood circumstances, an argument passed down to C code should not be modified if it has a positive reference count, even if that count is equal to one". For an example of when a reference count of 1 is not safe, consider: foo <- bar <- baz <- list(x = 42+0) # make a fresh numeric vector .Call(modify_me, foo$x) foo$x has a reference count of only 1, so NOT_SHARED() is true. On the other hand, since the bindings 'foo', 'bar', 'baz' all share the same list (whose reference count is 3), altering foo$x by reference from C code would also change the values of 'bar' and 'baz', which violates the value semantics of lists in R.2. Is the demonstrated behaviour a bug?In this particular case, you've shown the duplication could have been avoided, so at the very least you've got a feature request to make complex assignment more efficient. Now the question is, why does the duplication happen and how hard it is to avoid performing it without breaking anything? The complex assignment rules are described here: https://cran.r-project.org/doc/manuals/r-release/R-lang.html#Subset-assignment-1 If you call tracemem(env$vec) and set a breakpoint in memtrace_report(), you can see that env$vec is duplicated in eval.c, function evalseq(): (gdb) l evalseq (gdb) b 3201 Breakpoint 2 at 0x55555569dacb: file eval.c, line 3201. (gdb) commands 2call Rf_PrintValue(nexpr) c end(gdb) b 3209 Breakpoint 3 at 0x55555569dad3: file eval.c, line 3209. (gdb) commands 3call R_inspect(nval) call R_inspect(val) c endIn both cases, the expression being evaluated is `*tmp*`$vec, with `*tmp*` aliased to `env` without incrementing its reference count. When evaluating the first assignment, `env$vec[1] <- 5`, `nval` is the vector being updated, and `val` is a special, non-reference-counting pairlist containing `env` and `as.name("env")`: Breakpoint 3, evalseq <...> at eval.c:3209 # first the 'nval', note REF(1) @55555615f588 14 REALSXP g0c4 [REF(1)] (len=8, tl=0) 5,0,0,0,0,... # next the 'val', note REF(1) for its first element @555557df5fb0 02 LISTSXP g0c0 [STP] @555557d0a3b8 04 ENVSXP g0c0 [REF(1)] <0x555557d0a3b8> <...> @555555a2ae88 01 SYMSXP g0c0 [MARK,REF(1785)] "env" Next, after `env2 <- env`, we attempt an assignment again: Breakpoint 3, evalseq <...> at eval.c:3209 # again, 'nval' has a reference count of 1 @55555615f588 14 REALSXP g0c4 [REF(1)] (len=8, tl=0) 5,0,0,0,0,... # but now 'env' has a reference count of 2 @555557dfd108 02 LISTSXP g0c0 [STP] @555557d0a3b8 04 ENVSXP g0c0 [REF(2)] <0x555557d0a3b8> # <-- here <...> @555555a2ae88 01 SYMSXP g0c0 [MARK,REF(1787)] "env" Since `env` is referenced twice, it's MAYBE_SHARED, so the condition if (MAYBE_REFERENCED(nval) && (MAYBE_SHARED(nval) || MAYBE_SHARED(CAR(val)))) is true, and `nval` (env$x) is duplicated before the assignment. This would've been necessary if 'env' was a list (or another value-semantics object; see the first example above).3. I would guess that assign in place in this case is implementation-specific detail and not specified behaviour, so one shouldn't rely on it.True. R's copy-on-write is an optimisation, although a very useful one.4. Is there way how to do this (i.e., fixed buffer) in base R without relying on C with .Call?This is a kludge, but if you allow your environment to be enclosed by the base environment, you can perform the sub-assignment directly inside it, without invoking complex assignment: env3 <- new.env(parent = baseenv()) env3$vec <- vector("numeric", 8) tracemem(env3$vec) eval(substitute(vec[i] <- v, list(i = 1, v = 5)), env3) env4 <- env3 eval(substitute(vec[i] <- v, list(i = 2, v = 6)), env3) # still not duplicated (I've also tried substitute(..., list(`<-` = base::`<-`)) for use in an empty environment, but that breaks when it tries to invoke `[<-`.) What is the overall problem you would like to solve?______________________________________________ [email protected] mailing list https://stat.ethz.ch/mailman/listinfo/r-devel
______________________________________________ [email protected] mailing list https://stat.ethz.ch/mailman/listinfo/r-devel
