Существует ли в Lua ограничение на количество объектов / таблиц, которые могут быть детерминированно управляемы сборщиком мусора?

#memory #memory-management #lua #garbage-collection #free

#память #управление памятью #lua #сбор мусора #Бесплатно

Вопрос:

Для справки, сценарии, описанные ниже, были выполнены с использованием следующего:

  • Ubuntu 18.04.4 LTS (Bionic Beaver)
  • top «procps-ng 3.3.12»
  • Lua 5.3.3 Авторское право (C) 1994-2016 Lua.org , PUC-Рио

Каждый «сценарий» ниже демонстрирует результаты следующих шагов:

  1. Фиксируйте «lua5.3» начальное использование ресурсов процесса сверху
  2. В «lua5.3» выделите ~ 16 ГБ из кучи
  3. Зафиксируйте «lua5.3» использование ресурсов нового процесса из «top», чтобы показать, что было выделено 16 ГБ
  4. В «lua5.3» отметьте корневой объект для удаления, а затем вызовите «collectgarbage ()»
  5. Захват «lua5.3» конечного использования ресурсов процесса сверху, чтобы показать эффект «collectgarbage ()»

Для каждого сценария сравните начальное использование ресурсов процесса, «выделенное» использование ресурсов процесса и конечное использование ресурсов процесса после «collectgarbage(). Сценарий 1 показывает, что «выделенные» 16 ГБ НЕ освобождаются после сборки мусора. Сценарии 2 и 3 показывают, что часть «выделенных» 16 ГБ освобождается после сборки мусора. Сценарий 4 показывает, что все «выделенные» 16 ГБ освобождаются после сборки мусора. Сценарии с 1 по 4 отличаются только количеством объектов (т. Е. Таблиц), Используемых во время выделения, где сценарий 1 использует наибольшее количество таблиц (128 * 1024), а сценарий 4 использует наименьшее количество (1). Следовательно, похоже, что сборка мусора Lua 5.3.3 имеет ограничение на количество объектов, которыми он может действительно управлять, или есть дефект в управлении памятью Lua.

Короче говоря, сборка мусора Lua не показывает детерминированного поведения при управлении памятью. Если создается большое количество таблиц, использование памяти, связанное с этими таблицами, может никогда не вернуться в кучу, управляемую операционной системой, когда память больше не нужна в Lua. Однако, если создается небольшое количество таблиц, использование памяти, связанное с этими таблицами, похоже, ВСЕГДА возвращается в кучу, управляемую операционной системой, когда память больше не нужна в Lua.

Почему поведение управления памятью в Lua не является согласованным / детерминированным?

ПРИМЕЧАНИЕ: для каждого сценария вызов collectgarbage(«count») после collectgarbage() показывает, что lua5.3 правильно очистил весь мусор, тогда как использование «top» показывает, что это не всегда так. Следовательно, «top» используется здесь, чтобы показать истинное поведение сборки мусора Lua.

Сценарий 1: Lua collectgarbage() не освобождает используемую память

   PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME  COMMAND
22866 user      20   0   18152   3140   2840 S   0.0  0.0   0:00.00 lua5.3

> collectgarbage("count")
22.8759765625

> a = {} for i=0,256*1024 do a[i] = {} for j=0,4*1024*1024 do a[i][j] = i*j end end

> collectgarbage("count")
16803927.791016

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME  COMMAND
22866 user      20   0 16.053g 0.016t   2868 S   0.0 51.3   0:38.97 lua5.3

> a = nil collectgarbage()

> collectgarbage("count")
25.29296875

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME  COMMAND
22866 user      20   0 16.049g 0.016t   2868 S   0.0 51.3   0:39.08 lua5.3
 

Scenario 2: Lua collectgarbage() frees half of memory used

   PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME  COMMAND
22958 user      20   0   18152   3200   2916 S   0.0  0.0   0:00.00 lua5.3

> collectgarbage("count")
22.8759765625

> a = {} for i=0,128*1024 do a[i] = {} for j=0,8*1024*1024 do a[i][j] = i*j end end

> collectgarbage("count")
16790679.791016

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME  COMMAND
22958 user      20   0 16.284g 0.016t   2944 S   0.0 52.0   0:39.79 lua5.3

> a = nil collectgarbage()

> collectgarbage("count")
23.1826171875

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME  COMMAND
22958 user      20   0 8422324 8.018g   2944 S   0.0 25.6   0:40.50 lua5.3
 

Scenario 3: Lua collectgarbage() frees almost all memory used

   PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME  COMMAND
23127 user      20   0   18152   3192   2904 S   0.0  0.0   0:00.00 lua5.3

> collectgarbage("count")
22.8759765625

> a = {} for i=0,64*1024 do a[i] = {} for j=0,16*1024*1024 do a[i][j] = i*j end end

> collectgarbage("count")
16784151.791016

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME  COMMAND
23127 user      20   0 16.275g 0.016t   2932 S   0.0 52.0   0:41.22 lua5.3

> a = nil collectgarbage()

> collectgarbage("count")
23.1826171875

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME  COMMAND
23127 user      20   0   25900  10992   2932 S   0.0  0.0   0:41.81 lua5.3
 

Scenario 4: Lua collectgarbage() frees all memory used

   PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME  COMMAND
23771 user      20   0   18152   3204   2920 S   0.0  0.0   0:00.00 lua5.3

> collectgarbage("count")
22.8759765625

> a = {} for i=0,0 do a[i] = {} for j=0,1024*1024*1024 do a[i][j] = i*j end end

> collectgarbage("count")
16777241.969727

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME  COMMAND
23771 user      20   0 16.017g 0.016t   2948 S   0.0 51.2   0:38.97 lua5.3

> a = nil collectgarbage()

> collectgarbage("count")
23.17578125

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME  COMMAND
23771 user      20   0   18152   3364   2948 S   0.0  0.0   0:40.80 lua5.3
 

Update

After additional testing, it does appear that the number of objects (i.e. tables) managed in Lua affects the behavior of Lua’s garbage collection. Specifically, if the number of tables being managed by Lua is less than 256 (this seems to be the threshold), the memory will be returned to the operating system’s heap after garbage collection. As the number of objects grows above 256 (inclusive), Lua’s memory management begins to exhibit different behavior as it holds on to more memory for internal reuse after its garbage collection. Scenario 1 above shows the most dramatic affect of Lua holding onto memory after garbage collection, and Scenario 4 shows the least affect.

Note: The Example results are not deterministic; that is, other execution in Lua may affect the behavior of Lua’s garbage collection. For the most deterministic results, launch a new Lua process for each test.

For illustration, the following examples continuously allocate, populate, and deallocate a table of tables, printing out the following from the Lua process: iteration, preMemoryUsage (after allocation), preMemoryUsagePercent, postMemoryUsage (after deallocation), postMemoryUsagePercent.

Example Program: Change numberOfTables as Desired

 numberOfTables = 255 -- Example 1: 255; Example 2: 256

function getMemoryPercent()
  handle = io.popen("v=`ps -p $PPID -o pmem= | tr -d 'n' | sed 's/ *//g'`; echo -n $v")
  memoryPercent = handle:read("*a")
  handle:close()

  return memoryPercent
end

socket = require("socket")

print("#", "preMemUsage", "%", "postMemUsage", "%")

iteration = 0

while true
do
  a = {}
  
  for i = 1,numberOfTables
  do
    a[i] = {}

    for j = 1,math.floor(256*1024*1024/numberOfTables)
    do
      a[i][j] = i*j
    end
  end

  preMemoryUsage = collectgarbage("count")
  preMemoryUsagePercent = getMemoryPercent()

  a = nil
  collectgarbage()

  postMemoryUsage = collectgarbage("count")
  postMemoryUsagePercent = getMemoryPercent()

  iteration = iteration   1

  print(iteration, preMemoryUsage, preMemoryUsagePercent, postMemoryUsage, postMemoryUsagePercent)

end
 

Пример 1: Результаты для numberOfTables = 255

 #       preMemUsage     %       postMemUsage     %
1       8355905.0869141 25.4    47.240234375     0.0
2       8355905.1972656 25.4    47.322265625     0.0
3       8355905.1972656 25.4    47.322265625     0.0
4       8355905.1972656 25.4    47.322265625     0.0
5       8355905.1972656 25.4    47.322265625     0.0
6       8355905.1972656 25.4    47.322265625     0.0
7       8355905.1972656 25.4    47.322265625     0.0
8       8355905.1972656 25.4    47.322265625     0.0
9       8355905.1972656 25.4    47.322265625     0.0
10      8355905.1972656 25.4    47.322265625     0.0
 

Пример 2: Результаты для numberOfTables = 256

 #       preMemUsage     %       postMemUsage     %
1       4194369.0205078 12.8    47.119140625     0.0
2       4194369.1308594 12.8    47.201171875     12.8
3       4194369.1035156 12.8    47.173828125     12.5
4       4194369.1318359 12.8    47.2021484375    12.4
5       4194369.1318359 12.8    47.2021484375    12.4
6       4194369.1318359 12.8    47.2021484375    12.4
7       4194369.1318359 12.8    47.2021484375    12.4
8       4194369.1318359 12.8    47.2021484375    12.4
9       4194369.1318359 12.8    47.2021484375    12.4
10      4194369.1318359 12.8    47.2021484375    12.4
 

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

1. Вероятно, собранная память доступна для других объектов Lua, но до сих пор не возвращена в ОС. Можете ли вы повторить тесты, используя двойной вызов collectgarbage() ? Можете ли вы повторить тесты, используя collectgarbage("count") для получения использования памяти вместо выполнения top ?

2. @EgorSkriptunoff Я обновил запрошенную вами информацию. Повторные вызовы collectgarbage() не приводят к изменению поведения. Да, сначала я подумал, что это поведение типа Lua «виртуальная машина», которое позволяет Lua получать память из ОС, а затем просто управлять ею внутри. Но затем я понял, используя «top», что это поведение отличалось, когда я использовал небольшое количество таблиц. Кроме того, при использовании Lua в программе на C это проблематичная проблема управления памятью для процесса «C / Lua»; память, используемая в Lua, не всегда «освобождается» таким образом, чтобы часть «C» могла «повторно использовать» память.

3. Возможно, это не ошибка Lua, потому что Lua использует стандартный распределитель памяти из библиотеки C. Такое же поведение следует наблюдать, если вы работаете с кучей C вместо кучи Lua. И это означает, что память (которая не возвращается в ОС) может быть доступна для вашей программы на C.

4. Я выполнил аналогичный тест, используя только C, используя calloc() и free(); пока что это тестирование показало, что память возвращается в кучу, где «top» показывает, что память доступна. Для стороны Lua кажется, что я могу продолжать «повторно использовать» выделенную память в Lua. Но, как я уже сказал, бывают обстоятельства, при которых эта память может быть не в состоянии повторно использоваться C в смешанном процессе C / Lua.

5. Вам лучше опубликовать это в списке рассылки Lua.