Как R ссылается на неназначенные значения?

#r #memory-management

#r #управление памятью

Вопрос:

Я знаком с tracemem() отображением шестнадцатеричного адреса памяти назначенной переменной, например

 x <- 2
tracemem(x)
#> [1] "<0x876df68>"
  

но что это подразумевает (под капотом), когда значение буквально является просто неназначенным значением? например

 tracemem(4)
#> [1] "<0x9bd93b8>"
  

Тот же вопрос относится к простому вычислению выражения без присвоения

 4
#> [1] 4
  

Кажется, что если я оцениваю это несколько раз в консоли, я получаю постоянно увеличивающиеся шестнадцатеричные адреса

 tracemem(4)
#> [1] "<0x8779968>"
tracemem(4)
#> [1] "<0x87799c8>"
tracemem(4)
#> [1] "<0x8779a28>"
  

но если я либо явно выполняю эту операцию в цикле

 for ( i in 1:3 ) { print(tracemem(4)) }
#> [1] "<0x28bda48>"
#> [1] "<0x28bda48>"
#> [1] "<0x28bda48>"
  

или с sapply помощью replicate

 replicate(3, tracemem(4))
#> [1] "<0xba88208>" "<0xba88208>" "<0xba88208>"
  

Я получаю повторения одного и того же адреса, даже если я явно задерживаю печать между итерациями

 for ( i in 1:3 ) { print(tracemem(4)); Sys.sleep(1) }
#> [1] "<0xa3c4058>"
#> [1] "<0xa3c4058>"
#> [1] "<0xa3c4058>"
  

Мое лучшее предположение заключается в том, что вызов ссылается на уже временно присвоенное значение в parent.frame данном eval.parent(substitute( в replicate , но я недостаточно разбираюсь в базовом .Primitive коде for , чтобы знать, делает ли он там то же самое.

У меня есть некоторая уверенность, что R создает временные переменные, учитывая, что я могу сделать

 list(x = 1)
#> $x
#> [1] 1
  

таким образом, R должно быть, обрабатываются данные, даже если они никогда ничему не присваиваются. Я осведомлен о строгой формальности, изложенной в твите @hadleywickham:

введите описание изображения здесь

но я не уверен, как это работает здесь. Просто временное имя не сохраняется? Всегда ли for цикл использует это имя / объект? При вычислении большого количества кода, независимо от того, назначен он или нет, все еще расходуется память? (до тех пор, пока не будет вызван gc() , когда бы это ни было??)

tl; dr — как R «сохранять» неназначенные значения для печати?

Ответ №1:

Хорошо, я сделаю здесь все, что смогу.

Во-первых, tracemem — это примитив. Это означает, что это не замыкание, подобное подавляющему большинству функций уровня R, которые вы можете вызвать из R-кода. Более конкретно, это встроенный примитив XP:

 > .Internal(inspect(tracemem))
@62f548 08 BUILTINSXP g0c0 [MARK,NAM(1)] 
  

Это означает, что при его вызове НЕ применяется замыкание (поскольку это примитив), а вычисляется его аргумент, поскольку это ВСТРОЕННЫЙ XP (см. Этот раздел руководства по R internals).

Приложение закрытия — это когда объекты R, переданные в качестве аргументов при вызовах функций, присваиваются соответствующим переменным во фрейме вызова. Таким образом, этого не происходит для tracemem. Вместо этого его аргументы вычисляются на уровне C в SEXP, который никогда не привязывается к какому-либо символу в любой среде, а вместо этого передается непосредственно в функцию do_tracemem уровня C. Смотрите эту строку в функции оценки уровня C

Это означает, что когда числовая константа передается в tracemem (допустимый вызов, хотя обычно для этого нет никаких оснований), вы получаете фактический SEXP для константы, а не тот, который представляет переменную уровня R со значением 4, передаваемую в do_tracemem.

Насколько я могу судить, в любом фрейме оценки (возможно, я не совсем точно использую этот термин, но фреймы вызова и шаги в цикле for соответствуют требованиям, как и отдельные выражения верхнего уровня), каждая оценка, например, «4L» получает совершенно новый SEXP (в частности, INTSXP) с ИМЕНЕМ, сразу равным 4. Похоже, что между этими фреймами они могут быть общими, хотя я довольно сильно подозреваю, что это может быть артефактом повторного использования памяти, а не фактически разделяемыми секспами.

Приведенный ниже вывод, по-видимому, подтверждает теорию повторного использования памяти, но на данный момент у меня нет свободных циклов, чтобы подтвердить это сверх этого.

 > for(i in 1:3) {print(tracemem(4L)); print(tracemem(4L))}
[1] "<0x1c3f3b8>"
[1] "<0x1c3f328>"
[1] "<0x1c3f3b8>"
[1] "<0x1c3f328>"
[1] "<0x1c3f3b8>"
[1] "<0x1c3f328>"
  

Надеюсь, это поможет.

Комментарии:

1. Спасибо за содержательный и подробный ответ. Кроме того, замена непостоянной структуры в tracemem , похоже, делает то, что я ожидал, например, print(tracemem(c(5L))) в цикле для каждого вызова выдается другой адрес, предположительно потому, что (если я правильно понимаю ваш ответ) теперь он вычисляется с копией каждой итерации из-за того, что является оценкой закрытия (и, таким образом, ИМЯ увеличивается в do_for ).