2 Names and values

2.1 Introduction

In R, it is important to understand the distinction between an object and its name. Doing so will help you:

  • More accurately predict the performance and memory usage of your code.
  • Write faster code by avoiding accidental copies, a major source of slow code.
  • Better understand R’s functional programming tools.

The goal of this chapter is to help you understand the distinction between names and values, and when R will copy an object.

Quiz

Answer the following questions to see if you can safely skip this chapter. You can find the answers at the end of the chapter in Section 2.7.

  1. Given the following data frame, how do I create a new column called “3” that contains the sum of 1 and 2? You may only use $, not [[. What makes 1, 2, and 3 challenging as variable names?

  2. In the following code, how much memory does y occupy?

  3. On which line does a get copied in the following example?

Outline

  • Section 2.2 introduces you to the distinction between names and values, and discusses how <- creates a binding, or reference, between a name and a value.

  • Section 2.3 describes when R makes a copy: whenever you modify a vector, you’re almost certainly creating a new, modified vector. You’ll learn how to use tracemem() to figure out when a copy actually occurs. Then you’ll explore the implications as they apply to function calls, lists, data frames, and character vectors.

  • Section 2.4 explores the implications of the previous two sections on how much memory an object occupies. Since your intuition may be profoundly wrong and since utils::object.size() is unfortunately inaccurate, you’ll learn how to use lobstr::obj_size().

  • Section 2.5 describes the two important exceptions to copy-on-modify: with environments and values with a single name, objects are actually modified in place.

  • Section 2.6 concludes the chapter with a discussion of the garbage collector, which frees up the memory used by objects no longer referenced by a name.

Prerequisites

We’ll use the development version of lobstr to dig into the internal representation of R objects.

Sources

The details of R’s memory management are not documented in a single place. Much of the information in this chapter was gleaned from a close reading of the documentation (particularly ?Memory and ?gc), the memory profiling section of “Writing R extensions” (R Core Team 2018b), and the SEXPs section of “R internals” (R Core Team 2018a). The rest I figured out by reading the C source code, performing small experiments, and asking questions on R-devel. Any mistakes are entirely mine.

2.2 Binding basics

Consider this code:

It’s easy to read it as: “create an object named ‘x’, containing the values 1, 2, and 3”. Unfortunately, that’s a simplification that will lead to inaccurate predictions about what R is actually doing behind the scenes. It’s more accurate to say that this code is doing two things:

  • It’s creating an object, a vector of values, c(1, 2, 3).
  • And it’s binding that object to a name, x.

In other words, the object, or value, doesn’t have a name; it’s actually the name that has a value.

To further clarify this distinction, I’ll draw diagrams like this:

The name, x, is drawn with a rounded rectangle. It has an arrow that points to (or binds or references) the value, the vector 1:3. Note that the arrow points in opposite direction to the assignment arrow: <- creates a binding from the name on the left-hand side to the object on the right-hand side.

Thus, you can think of a name as a reference to a value. For example, if you run this code, you don’t get another copy of the value 1:3, you get another binding to the existing object:

You might have noticed that the value 1:3 has a label: 0x74b. While the vector doesn’t have a name, I’ll occasionally need to refer to an object independent of its bindings. To make that possible, I’ll label values with a unique identifier. These identifiers have a special form that looks like the object’s memory “address”, i.e. the location in memory where the object is stored. But because the actual memory addresses changes every time the code is run, we use these identifiers instead.

You can access an object’s identifier with lobstr::obj_addr(). Doing so allows you to see that both x and y point to the same identifier:

These identifiers are long, and change every time you restart R.

It can take some time to get your head around the distinction between names and values, but understanding this is really helpful in functional programming where functions can have different names in different contexts.

2.2.1 Non-syntactic names

R has strict rules about what constitutes a valid name. A syntactic name must consist of letters2, digits, . and _ but can’t begin with _ or a digit. Additionally, you can’t use any of the reserved words like TRUE, NULL, if, and function (see the complete list in ?Reserved). A name that doesn’t follow these rules is a non-syntactic name; if you try to use them, you’ll get an error:

It’s possible to override these rules and use any name, i.e., any sequence of characters, by surrounding it with backticks:

While it’s unlikely you’d deliberately create such crazy names, you need to understand how these crazy names work because you’ll come across them, most commonly when you load data that has been created outside of R.

2.2.2 Exercises

  1. Explain the relationship between a, b, c and d in the following code:

  2. The following code accesses the mean function in multiple ways. Do they all point to the same underlying function object? Verify this with lobstr::obj_addr().

  3. By default, base R data import functions, like read.csv(), will automatically convert non-syntactic names to syntactic ones. Why might this be problematic? What option allows you to suppress this behaviour

  4. What rules does make.names() use to convert non-syntactic names into syntactic ones?

  5. I slightly simplified the rules that govern syntactic names. Why is .123e1 not a syntactic name? Read ?make.names for the full details.

2.3 Copy-on-modify

Consider the following code. It binds x and y to the same underlying value, then modifies y.3

Modifying y clearly didn’t modify x. So what happened to the shared binding? While the value associated with y changed, the original object did not. Instead, R created a new object, 0xcd2, a copy of 0x74b with one value changed, then rebinded y to that object.

This behaviour is called copy-on-modify. Understanding it will radically improve your intuition about the performance of R code. A related way to describe this behaviour is to say that R objects are unchangeable, or immutable. However, I’ll generally avoid that term because there are a couple of important exceptions to copy-on-modify that you’ll learn about in Section 2.5.

2.3.1 tracemem()

You can see when an object gets copied with the help of base::tracemem(). Once you call that function with an object, you’ll get the object’s current address:

From then on, whenever that object is copied, tracemem() will print a message telling you which object was copied, its new address, and the sequence of calls that led to the copy:

Note that if you modify y again, it won’t get copied. That’s because the new object now only has a single name bound to it, so R applies modify-in-place optimisation. We’ll come back to this shortly.

untracemem() is the opposite of tracemem(); it turns tracing off.

2.3.2 Function calls

The same rules for copying also apply to function calls. Take this code:

While f() is running, the a inside the function points to the same value as the x does outside the function:

(You’ll learn more about the conventions used in this diagram in Execution environments.)

Once f() completes, x and z will point to the same object. 0x74b never gets copied because it never gets modified. If f() did modify x, R would create a new copy, and then z would bind that object.

2.3.3 Lists

It’s not just names (i.e. variables) that point to values; elements of lists do too. Take this list, which is superficially very similar to the vector above:

The internal representation of a list is actually quite different from that of a vector. A list is really a vector of references:

This is particularly important when we modify a list:

Like vectors, lists use copy-on-modify behaviour; the original list is left unchanged, and R creates a modified copy. This, however, is a shallow copy: the list object and its bindings are copied, but the values pointed to by the bindings are not. The opposite of a shallow copy is a deep copy where the contents of every reference are copied. Prior to R 3.1.0, copies were always deep copies.

To see values that are shared across lists, use lobstr::ref(). ref() prints the memory address of each object, along with a local ID so that you can easily cross-reference shared components.

2.3.4 Data frames

Data frames are lists of vectors, so copy-on-modify has important consequences when you modify a data frame. Take this data frame as an example:

If you modify a column, only that column needs to be modified; the others will still point to their original references:

However, if you modify a row, there is no way to share data with the previous version of the data frame: every column must be copied-and-modified.

2.3.5 Character vectors

The final place that R uses references is with character vectors. I usually draw character vectors like this:

But this is a polite fiction. R actually uses a global string pool where each element of a character vector is a pointer to a unique string in the pool:

You can request that ref() show these references by setting the character argument to TRUE:

This has a profound impact on the amount of memory a character vector uses but is otherwise generally unimportant, so elsewhere in the book I’ll draw character vectors as if the strings lived inside a vector.

2.3.6 Exercises

  1. Why is tracemem(1:10) not useful?

  2. Explain why tracemem() shows two copies when you run this code. Hint: carefully look at the difference between this code and the code shown earlier in the section.

  3. Sketch out the relationship between the following objects:

  4. What happens when you run this code?

    Draw a picture.

2.4 Object size

You can find out how much memory an object takes with lobstr::obj_size()4:

Since the elements of lists are references to values, the size of a list might be much smaller than you expect:

y is only 80 bytes5 bigger than x. That’s the size of an empty list with three elements:

Similarly, because R uses a global string pool character vectors take up less memory than you might expect: repeating a string 1000 times does not make it take up 1000 times as much memory.

References also make it challenging to think about the size of individual objects. obj_size(x) + obj_size(y) will only equal obj_size(x, y) if there are no shared values. Here, the combined size of x and y is the same as the size of y:

2.4.1 Exercises

  1. In the following example, why are object.size(y) and obj_size(y) so radically different? Consult the documentation of object.size().

  2. Take the following list. Why is its size somewhat misleading?

  3. Predict the output of the following code:

2.5 Modify-in-place

As we’ve seen above, modifying an R object usually creates a copy. There are two exceptions:

  • Objects with a single binding get a special performance optimisation.

  • Environments, a special type of object, are always modified in place.

2.5.1 Objects with a single binding

If an object has a single name bound to it, R will modify it in place:

(Note the object IDs here: v continues to bind to the same object, 0x207.)

Two complications make predicting exactly when R applies this optimisation challenging:

  • When it comes to bindings, R can currently6 only count 0, 1, or many. That means that if an object has two bindings, and one goes away, the reference count does not go back to 1: one less than many is still many.

  • Whenever you call any regular function, it makes a reference to the object. The only exception are specially written C functions. These occur mostly in the base package.

Together, these two complications make it hard to predict whether or not a copy will occur. Instead, it’s better to determine it empirically with tracemem().

Let’s explore the subtleties with a case study using for loops. For loops have a reputation for being slow in R, but often that slowness is caused by every iteration of the loop creating a copy. Consider the following code. It subtracts the median from each column of a large data frame:

This loop is surprisingly slow because each iteration of the loop copies the data frame. You can see this by using tracemem():

In fact, each iteration copies the data frame not once, not twice, but three times! Two copies are made by [[.data.frame, and a further copy7 is made because [[.data.frame is a regular function that increments the reference count of x.

We can reduce the number of copies by using a list instead of a data frame. Modifying a list uses internal C code, so the refs are not incremented and only a single copy is made:

While it’s not hard to determine when a copy is made, it is hard to prevent it. If you find yourself resorting to exotic tricks to avoid copies, it may be time to rewrite your function in C++, as described in Chapter 23.

2.5.2 Environments

You’ll learn more about environments in Chapter 6, but it’s important to mention them here because their behavior is different from that of other objects: environments are always modified in place. This property is sometimes described as reference semantics because when you modify an environment all existing bindings to that environment continue to have the same reference.

Take this environment, which we bind to e1 and e2:

If we change a binding, the environment is modified in place:

This basic idea can be used to create functions that “remember” their previous state. See Section 9.2.3 for more details. This property is also used to implement the R6 object oriented programming system, the topic of Chapter 13.

One consequence of this is that environments can contain themselves:

This is a unique property of environments!

2.5.3 Exercises

  1. Wrap the two methods for subtracting medians into two functions, then use the ‘bench’ package (Hester 2018) to carefully compare their speeds. How does performance change as the number of columns increase?

  2. What happens if you attempt to use tracemem() on an environment?

2.6 Unbinding and the garbage collector

Consider this code:

We create two objects, but by the end of code neither object is bound to a name. How do these objects get deleted? That’s the job of the garbage collector, or GC, for short. The GC creates more memory by deleting R objects that are no longer used, and if needed, requesting more memory from the operating system.

R uses a tracing GC. That means it traces every object reachable from the global8 environment, and all the objects reachable from those objects (i.e. the references in lists and environments are searched recursively). The garbage collector does not use the reference count used for the modify-in-place optimisation described above. The two ideas are closely related but the internal data structures have been optimised for different use cases.

The garbage collector (GC) is run automatically whenever R needs more memory to create a new object. From the outside, it’s basically impossible to predict when the GC will run, and indeed, you shouldn’t try. Instead, if you want to find out when the GC runs, call gcinfo(TRUE): then the GC will print a message to the console every time it runs.

You can force the garbage collector to run by calling gc(). Despite what you might have read elsewhere, there’s never any need to call gc() yourself. You may want to call gc() to ask R to return memory to your operating system, or for its side-effect of telling you how much memory is currently being used:

lobstr::mem_used() is a wrapper around gc() that just prints the total number of bytes used:

This number won’t agree with the amount of memory reported by your operating system for three reasons:

  1. It only includes objects created by R, not the R interpreter itself.

  2. Both R and the operating system are lazy: they won’t reclaim memory until it’s actually needed. R might be holding on to memory because the OS hasn’t yet asked for it back.

  3. R counts the memory occupied by objects but there may be gaps due to deleted objects. This problem is known as memory fragmentation.

2.7 Answers

  1. You must surround non-syntactic names in `. The variables 1, 2, and 3 have non-syntactic names, so must always be quoted with backticks.

  2. It occupies about 8 MB.

  3. a is copied when b is modified, b[[1]] <- 10.

References

R Core Team. 2018b. “Writing R Extensions.” R Foundation for Statistical Computing. https://cran.r-project.org/doc/manuals/r-devel/R-ints.html.

R Core Team. 2018a. “R Internals.” R Foundation for Statistical Computing. https://cran.r-project.org/doc/manuals/r-devel/R-exts.html.

Hester, Jim. 2018. Bench: High Precision Timing of R Expressions. http://bench.r-lib.org/.


  1. Surprisingly, what constitutes a letter is determined by your current locale. That means that the syntax of R code can actually differ from computer to computer, and that it’s possible for a file that works on one computer to not even parse on another!

  2. You may be surprised to see [[ used with a numeric vector. We’ll come back to this in Section 4.3, but in brief, I think you should always use [[ when you are getting or setting a single element.

  3. Beware of the utils::object.size() function. It does not correctly account for shared references and will return sizes that are too large.

  4. If you’re running the 32-bit version of R you’ll see slightly different sizes.

  5. By the time you read this, this may have changed, as plans are afoot to improve reference counting: https://developer.r-project.org/Refcnt.html

  6. Note that these copies are shallow: they only copy the reference to each individual column, not the contents of the columns. This means the performance isn’t terrible, but it’s obviously not as good as it could be.

  7. And every environment on the current call stack.