I'm having a little trouble understanding the pass-by-reference properties of data.table
. Some operations seem to 'break' the reference, and I'd like to understand exactly what's happening.
On creating a data.table
from another data.table
(via <-
, then updating the new table by :=
, the original table is also altered. This is expected, as per:
?data.table::copy
and stackoverflow: pass-by-reference-the-operator-in-the-data-table-package
Here's an example:
library(data.table)
DT <- data.table(a=c(1,2), b=c(11,12))
print(DT)
# a b
# [1,] 1 11
# [2,] 2 12
newDT <- DT # reference, not copy
newDT[1, a := 100] # modify new DT
print(DT) # DT is modified too.
# a b
# [1,] 100 11
# [2,] 2 12
However, if I insert a non-:=
based modification between the <-
assignment and the :=
lines above, DT
is now no longer modified:
DT = data.table(a=c(1,2), b=c(11,12))
newDT <- DT
newDT$b[2] <- 200 # new operation
newDT[1, a := 100]
print(DT)
# a b
# [1,] 1 11
# [2,] 2 12
So it seems that the newDT$b[2] <- 200
line somehow 'breaks' the reference. I'd guess that this invokes a copy somehow, but I would like to understand fully how R is treating these operations, to ensure I don't introduce potential bugs in my code.
I'd very much appreciate if someone could explain this to me.
Just a quick sum up.
<-
withdata.table
is just like base; i.e., no copy is taken until a subassign is done afterwards with<-
(such as changing the column names or changing an element such asDT[i,j]<-v
). Then it takes a copy of the whole object just like base. That's known as copy-on-write. Would be better known as copy-on-subassign, I think! It DOES NOT copy when you use the special:=
operator, or theset*
functions provided bydata.table
. If you have large data you probably want to use them instead.:=
andset*
will NOT COPY thedata.table
, EVEN WITHIN FUNCTIONS.Given this example data :
The following just "binds" another name
DT2
to the same data object bound currently bound to the nameDT
:This never copies, and never copies in base either. It just marks the data object so that R knows that two different names (
DT2
andDT
) point to the same object. And so R will need to copy the object if either are subassigned to afterwards.That's perfect for
data.table
, too. The:=
isn't for doing that. So the following is a deliberate error as:=
isn't for just binding object names ::=
is for subassigning by reference. But you don't use it like you would in base :you use it like this :
That changed
DT
by reference. Say you add a new columnnew
by reference to the data object, there is no need to do this :because the RHS already changed
DT
by reference. The extraDT <-
is to misunderstand what:=
does. You can write it there, but it's superfluous.DT
is changed by reference, by:=
, EVEN WITHIN FUNCTIONS :data.table
is for large datasets, remember. If you have a 20GBdata.table
in memory then you need a way to do this. It's a very deliberate design decision ofdata.table
.Copies can be made, of course. You just need to tell data.table that you're sure you want to copy your 20GB dataset, by using the
copy()
function :To avoid copies, don't use base type assignation or update :
If you want to be sure that you are updating by reference use
.Internal(inspect(x))
and look at the memory address values of the constituents (see Matthew Dowle's answer).Writing
:=
inj
like that allows you subassign by reference by group. You can add a new column by reference by group. So that's why:=
is done that way inside[...]
:Yes, it's subassignment in R using
<-
(or=
or->
) that makes a copy of the whole object. You can trace that usingtracemem(DT)
and.Internal(inspect(DT))
, as below. Thedata.table
features:=
andset()
assign by reference to whatever object they are passed. So if that object was previously copied (by a subassigning<-
or an explicitcopy(DT)
) then it's the copy that gets modified by reference.Notice how even the
a
vector was copied (different hex value indicates new copy of vector), even thougha
wasn't changed. Even the whole ofb
was copied, rather than just changing the elements that need to be changed. That's important to avoid for large data, and why:=
andset()
were introduced todata.table
.Now, with our copied
newDT
we can modify it by reference :Notice that all 3 hex values (the vector of column points, and each of the 2 columns) remain unchanged. So it was truly modified by reference with no copies at all.
Or, we can modify the original
DT
by reference :Those hex values are the same as the original values we saw for
DT
above. Typeexample(copy)
for more examples usingtracemem
and comparison todata.frame
.Btw, if you
tracemem(DT)
thenDT[2,b:=600]
you'll see one copy reported. That is a copy of the first 10 rows that theprint
method does. When wrapped withinvisible()
or when called within a function or script, theprint
method isn't called.All this applies inside functions too; i.e.,
:=
andset()
do not copy on write, even within functions. If you need to modify a local copy, then callx=copy(x)
at the start of the function. But, rememberdata.table
is for large data (as well as faster programming advantages for small data). We deliberately don't want to copy large objects (ever). As a result we don't need to allow for the usual 3* working memory factor rule of thumb. We try to only need working memory as large as one column (i.e. a working memory factor of 1/ncol rather than 3).