Names and Values

Applying R to Lifestyle and Brain Health Research

Brian C. Helsel, PhD

University of Kansas Medical Center

August 12, 2026

Binding Basics

library(lobstr)
x <- c(1, 2, 3)
obj_addr(x)
#> 0x8d526b868

Behind the scenes R creates a vector object of values c(1, 2, 3) and binds it to a name x with an object address of 0x8d526b868.

# Creates another binding to the same object
y <- x
obj_addr(y)
#> 0x8d526b868

Copy-on-modify

y[[3]] <- 4
obj_addr(y)
#> 0x8d56e6968

Modifying y creates a new object that is a copy of 0x8d526b868 and has a new object address of 0x8d56e6968.

We can see when an object gets copied with base::tracemem()

y <- x
tracemem(x)
y[[3]] <- 4
untracemem(x)
# echo: false

paste0("<", obj_addr(x), ">")
# echo: false

paste0("tracemem[", obj_addr(x), " -> ", obj_addr(y), "]:")

Function Calls

The same rules for copying apply to function calls.

f <- function(a) {
  a
}

tracemem(x)

z <- f(x)

untracemem(x)

# echo: false

paste0("<", obj_addr(x), ">")

Once f() completes, x and z will point to the same object.

# echo: false

f <- function(a) {
  a
}

z <- f(x)

paste0("tracemem[", obj_addr(x), " -> ", obj_addr(z), "]:")

Lists

Elements of lists can also point to objects, but copy-on-modify differs from variables in that a shallow copy is created.

l1 <- list(1, 2, 3)

A list stores references to the values rather than the values. This is important to know for understanding what happens when we modify a list.

l2 <- l1

l2[[3]] <- 4

Using the ref() function from the lobstr package shows the shared bindings across l1 and l2.

ref(l1, l2)

Data frames

Data frames are lists of vectors and copy-on-modify works differently for columns and rows.

d1 <- data.frame(x = c(1, 5, 6), y = c(2, 4, 3))

Columns

Only the modified column is changed.

d2 <- d1
d2[, 2] <- d2[, 2] * 2

Rows

Every column is modified and must be copied.

d3 <- d1
d3[1, ] <- d3[1, ] * 3

Character Vectors

R uses a global string pool where each element in a character vector points to a unique string in the pool.

x <- c("a", "a", "abc", "d")
ref(x, character = TRUE)

The global string pool has an impact on the amount of memory a character vector uses, but can otherwise be thought of similarly to numeric vectors.

Object Size

Use lobstr::obj_size to find out how much memory an object takes.

x <- rnorm(1000, mean = 35, sd = 5)
y <- list(x, x, x)
x_size <- obj_size(x)
y_size <- obj_size(y)

sprintf(
  "y (size: %s B) is only %s B larger than x (size: %s B)",
  y_size,
  y_size - x_size,
  x_size
)

The elements of a list are references to the values. Storing x three times is the size of an empty list with three NULL elements.

obj_size(list(NULL, NULL, NULL))

Character vectors also take up less memory than expected.

fruits <- c("apple", "apple", "apple")

sprintf(
  "Repeating fruit 100 times takes up %s B more in memory",
  obj_size(rep(fruits, 100)) - obj_size(fruits)
)

Object Size for Shared Values

References make it more challenging to think about the size of individual objects. The object size only equals the combined object size if there are no shared values.

vegetables <- c("broccoli", "broccoli", "broccoli")
fruits_or_vegetables <- c("apple", "broccoli", "apple")

obj_size(fruits, vegetables)
#> 280 B
obj_size(fruits) + obj_size(vegetables)
#> 280 B
obj_size(fruits, fruits_or_vegetables)
#> 280 B
obj_size(fruits) + obj_size(fruits_or_vegetables)
#> 336 B

Alternative Representation

R has the ability to represent some vectors compactly. For example, using the : when providing a numeric range stores the first and last number only such that every sequence is always the same size.

print(1:10)
obj_size(1:10)
#> 680 B
obj_size(1:1000)
#> 680 B
obj_size(1:1000000)
#> 680 B

Environments

R environments are modified-in-place rather than copied-on-modify. When you modify an environment, all existing bindings to that environment continue to have the same reference.

e1 <- rlang::env(a = 1, b = 2, c = 3)
e2 <- e1

# e2$c changes with the modification of e1$c
e1$c <- 4

print(e2$c)
#>  4

Unbinding and the garbage collector

# Assigns x to
# numeric object
x <- 1:3

# Reassigns x to new
# numeric object
x <- 2:4

# Unbinds x from
# numeric objects
rm(x)

After running this code, we have two numeric objects not bound to a name. The garbage collector (GC) frees up memory by deleting R objects that are no longer used. The garbage collector runs automatically when R needs more memory to create new objects or manually using the gc function. You can also check the memory used using the lobstr::mem_used function.

gc()

mem_used()