Linux использует COW для поддержания использования памяти на низком уровне после ветвления, но пути Perl 5 работ переменных в perl
кажется, побеждает эту оптимизацию. Например, для переменной:
my $s = "1";
perl
действительно хранит:
SV = PV(0x100801068) at 0x1008272e8
REFCNT = 1
FLAGS = (POK,pPOK)
PV = 0x100201d50 "1"\0
CUR = 1
LEN = 16
При использовании той строки в числовом контексте она изменяет C struct
представление данных:
SV = PVIV(0x100821610) at 0x1008272e8
REFCNT = 1
FLAGS = (IOK,POK,pIOK,pPOK)
IV = 1
PV = 0x100201d50 "1"\0
CUR = 1
LEN = 16
Сам указатель строки не изменился (тихо 0x100201d50
), но теперь это находится в другом C struct
(a PVIV
вместо a PV
). Я не изменил значение вообще, но внезапно я оплачиваю стоимость COW. Есть ли любой способ заблокировать perl
представление переменной Perl 5 так, чтобы эта экономия времени (perl
не должен преобразовывать "0"
кому: 0
во второй раз), взлом не повреждает мое использование памяти?
Отметьте, представления выше были сгенерированы из этого кода:
perl -MDevel::Peek -e '$s = "1"; Dump $s; $s + 0; Dump $s'
Единственное решение, которое я нашел на данный момент, это заставить perl
выполнять все преобразования, которые я ожидаю, в родительском процессе. И как видно из приведенного ниже кода, даже это лишь немного помогает.
Results:
Useless use of addition (+) in void context at z.pl line 34.
Useless use of addition (+) in void context at z.pl line 45.
Useless use of addition (+) in void context at z.pl line 51.
before eating memory
used memory: 71
after eating memory
used memory: 119
after 100 forks that don't reference variable
used memory: 144
after children are reaped
used memory: 93
after 100 forks that touch the variables metadata
used memory: 707
after children are reaped
used memory: 93
after parent has updated the metadata
used memory: 109
after 100 forks that touch the variables metadata
used memory: 443
after children are reaped
used memory: 109
Code:
#!/usr/bin/perl
use strict;
use warnings;
use Parallel::ForkManager;
sub print_mem {
print @_, "used memory: ", `free -m` =~ m{cache:\s+([0-9]+)}s, "\n";
}
print_mem("before eating memory\n");
my @big = ("1") x (1_024 * 1024);
my $pm = Parallel::ForkManager->new(100);
print_mem("after eating memory\n");
for (1 .. 100) {
next if $pm->start;
sleep 2;
$pm->finish;
}
print_mem("after 100 forks that don't reference variable\n");
$pm->wait_all_children;
print_mem("after children are reaped\n");
for (1 .. 100) {
next if $pm->start;
$_ + 0 for @big; #force an update to the metadata
sleep 2;
$pm->finish;
}
print_mem("after 100 forks that touch the variables metadata\n");
$pm->wait_all_children;
print_mem("after children are reaped\n");
$_ + 0 for @big; #force an update to the metadata
print_mem("after parent has updated the metadata\n");
for (1 .. 100) {
next if $pm->start;
$_ + 0 for @big; #force an update to the metadata
sleep 2;
$pm->finish;
}
print_mem("after 100 forks that touch the variables metadata\n");
$pm->wait_all_children;
print_mem("after children are reaped\n");
В любом случае, если вы избегаете COW на старте и во время работы, вы не должны забывать о фазе END времени жизни. В выключении есть две фазы GC, когда в первой происходит обновление счетчиков ссылок, так что это может убить вас по-хорошему. Вы можете решить эту проблему следующим образом:
END { kill 9, $$ }
Это само собой разумеется, но COW происходит не на основе отдельных структур, а на основе страниц памяти. Так что достаточно, чтобы одна вещь на всей странице памяти была изменена таким образом, чтобы вы оплатили стоимость копирования.
В Linux вы можете запросить размер страницы следующим образом:
getconf PAGESIZE
В моей системе это 4096 байт. В это пространство можно уместить множество скалярных структур Perl. Если что-то изменится, Linux придется скопировать все.
Вот почему использование арен памяти в целом является хорошей идеей. Вам следует разделить изменяемые и неизменяемые данные, чтобы вам не пришлось оплачивать COW за неизменяемые данные только потому, что они оказались на той же странице памяти, что и изменяемые данные.