Эта VM использует moving garbage collector, т.е. объекты гуляют по памяти когда происходит GC.
Вот пример кода
typedef uint64 value; // if object then contains pointer to memory
value csf_native_function(VM* c, value obj, value p1, value p2) {
if( !cs_is_string(p1) )
p1 = cs_allocate_string(c, ' ', 256); // may cause GC()
// obj, p1, p2 could be wrong at this point due to GC in the line abovereturn csf_other_func(c, obj, p1, p2);
}
Т.к. cs_allocate_string() может вызывать GC() то значения obj, p1, p2 могут быть не валидными —
указывать на старые локации объектов.
Решаю эту проблему с помощью специадьного RAII объекта PROTECT:
value csf_native_function(VM* c, value obj, value p1, value p2) {
PROTECT(c, obj, p1, p2);
if( !cs_is_string(p1) )
p1 = cs_allocate_string(c, ' ', 256); // may cause GC()
// obj, p1, p2 could be wrong at this point due to GC in the line abovereturn csf_other_func(c, obj, p1, p2);
}
Проблема в том что это PROTECT() нужно задавать явно. Иногда ошибаюсь и ловить проблему приходится долго и нудно.
Хочется построить что-то типа консервативного GC чтобы шерстил С/C++ стек на предмет чего-то похожего на pointers.
Но что-то мне говорит что ничего хорошего их этого не получится. Всякие там оптимизации, размещение переменных в регистрах и прочая.
Что-то еще можно придумть на эту тему? Приветствуются идеи любой степени сумашествия.
Я не вполне понял, как твоя механика работает, но можно было бы ввести два типа -- один для свободных объектов, а второй для приколотых.
И сделать доступ к приколотым объектам через RAII. Тогда не будешь ошибаться.
Здравствуйте, Шахтер, Вы писали:
Ш>Здравствуйте, c-smile, Вы писали:
Ш>Я не вполне понял, как твоя механика работает, но можно было бы ввести два типа -- один для свободных объектов, а второй для приколотых. Ш>И сделать доступ к приколотым объектам через RAII. Тогда не будешь ошибаться.
А как тогда сигнатура такой функции должна выглядеть?
value csf_native_function(VM* c, value obj, value p1, value p2) {
if( !cs_is_string(p1) )
p1 = cs_allocate_string(c, ' ', 256); // may cause GC()
// obj, p1, p2 could be wrong at this point due to GC in the line abovereturn csf_other_func(c, obj, p1, p2);
}
проблема в том что VM может быть несколько. Т.е. чтобы сконструировать raii holder нужно VM* и само value:
Передавать managed_values в параметрах как бы накладно, да и неудобно.
В качестве дикой идеи пробовал тупо сканировать C стек на предмет чего-то похожего на value-address, но в отличии от Boehm collector
у меня compacting GC. Т.е. нужно не только сканировать стек но и модифицировать его грязными руками.
Предварительные тесты показали жуткие результаты. Особенно с включенными оптимизациями и все такое.
Эх, что-то мне говорит что так и придется это всё руками продолжать делать...
Но с ним как раз проблема та что я описал изначально — приходится помнить что если там может оказаться GCable thing то надо его pin.
В этом то и проблема что нужно очень аккуратно с этим делом.
Здравствуйте, c-smile, Вы писали:
CS>Что-то еще можно придумть на эту тему? Приветствуются идеи любой степени сумашествия.
ничего сумасшедшего посоветовать не могу — используй подход managed С++. ключевые слова gcroot, GC.KeepAlive, GCHandle, __pin
CS>value csf_native_function(VM* c, value obj, value p1, value p2) {
CS> PROTECT(c, obj, p1, p2);
CS>
1) А тебе не кажется, что залочить obj, p1 и p2 должны при вызове функции? А то мало ли откуда они пришли?
2) Я бы сделал такой прокси-объект, который в конструкторе лочит, в деструкторе отпускает, и умеет каститься к ссылке на указуемое, и в operator-> к указателю, на него же.
Тогда при вызове можно будет отдавать прокси, который скастится куда надо.
Одна проблема, как передавать VM*. Ну можно через статический на нить параметр, например.
Но главная проблема в другом, так жить-то нельзя? Так придётся всё лочить, что на стеке лежит, что противоречит самой идее GC.
Как в той VM вообще жить предлагается? Может можно как-то в ней регить указатели на GC-объекты, или есть какие-то стабильные хэндлы, например, или ещё какая-то стратегия?
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, Erop, Вы писали:
E>Здравствуйте, c-smile, Вы писали:
CS>>
CS>>value csf_native_function(VM* c, value obj, value p1, value p2) {
CS>> PROTECT(c, obj, p1, p2);
CS>>
E>1) А тебе не кажется, что залочить obj, p1 и p2 должны при вызове функции? А то мало ли откуда они пришли?
В бощем и целом — нет, caller ничего не знает про то что функция делает. Если она только читает то ничего лочить не надо.
Проблема в том что GC происходит редко. Идеально никакой допополнительный код не должен исполнятся если не происходит GC. Т.е. zero overhead.
E>2) Я бы сделал такой прокси-объект, который в конструкторе лочит, в деструкторе отпускает, и умеет каститься к ссылке на указуемое, и в operator-> к указателю, на него же.
E>Но главная проблема в другом, так жить-то нельзя? Так придётся всё лочить, что на стеке лежит, что противоречит самой идее GC.
В момент вызова таких функций известен диапазон C/C++ стека где такие value находятся.
Т.е. идеально было бы что-то типа этого
void vm::collect_garbage() {
...
value dummy;
void* p = &dummy; // c stack top void* pend = c_stack_bottom;
for(; p < pend; ++p )
if( looks_like_value_address(p))
*((value*)p) = copy_value( *((value*)p) );
...
}
Но все дело в функции looks_like_value_address() — она, в отличие от Boehm GC ошибаться не имеет права — стек попортится.
E>Как в той VM вообще жить предлагается? Может можно как-то в ней регить указатели на GC-объекты, или есть какие-то стабильные хэндлы, например, или ещё какая-то стратегия?
Да нормально оно всё живет (в Sciter, уже лет шесть как), есть pinned_value's которые держат объекты периметра. Для стека же у меня есть PROTECT(list of variables).
Просто при добавлении новых функций приходится быть предельно аккуратным и не забывать делать PROTECT().
Недавно вот ловил багу которая очень хитро и спорадически воспроизводилась. GC() он достаточно редко срабатывает и момент срабатывания зависит от входных данных — мечта дебагера.
Здравствуйте, c-smile, Вы писали:
CS>Проблема в том что это PROTECT() нужно задавать явно. Иногда ошибаюсь и ловить проблему приходится долго и нудно.
CS>Что-то еще можно придумть на эту тему? Приветствуются идеи любой степени сумашествия.
Ну конкретно эти две строчки можно упаковать в единый макрос: CS>
CS>value csf_native_function(VM* c, value obj, value p1, value p2) {
CS> PROTECT(c, obj, p1, p2);
CS>
либо внутри PROTECT заюзать __PRETTY_FUNCTION__ и сравнить ее конец с __FUNCTION__ "(" (тут цикл abi::cxx_demangle(typeid<decltype(obj)>().name()) по всем аргументам PROTECT) ")"
это все для случая, который ты показал — что все, что нужно защитить, есть в аргументах.
Если нужно защищать еще какие-то локальные объекты на стеке, возникающие по мере выполнения функции — тогда надо их объявление упаковать в макрос, который сразу же после объявления сделает ему PROTECT.
Здравствуйте, c-smile, Вы писали:
CS>Недавно вот ловил багу которая очень хитро и спорадически воспроизводилась. GC() он достаточно редко срабатывает и момент срабатывания зависит от входных данных — мечта дебагера.
Ну эта конкретная проблема решается специальной отладочной версией GC, который, во-первых, всегда срабатывает, а во-вторых, опять же всегда двигает все объекты в памяти, причем желательно двигать в какой-нть мусор, чтоб прога точно навернулась.
Здравствуйте, c-smile, Вы писали:
CS>Т.к. cs_allocate_string() может вызывать GC() то значения obj, p1, p2 могут быть не валидными — CS>указывать на старые локации объектов. CS>Решаю эту проблему с помощью специадьного RAII объекта PROTECT:
Какой у тебя эффект от PROTECT? Эти указатели обновляются после перемещения объектов или объекты по указателям не перемещаются?
Здравствуйте, Evgeny.Panasyuk, Вы писали:
CS>>Решаю эту проблему с помощью специадьного RAII объекта PROTECT:
EP>Какой у тебя эффект от PROTECT? Эти указатели обновляются после перемещения объектов или объекты по указателям не перемещаются?
Значения переменных могут изменятся. После cs_push_var_address их адреса становятся известными GC процессору. Поэтому если объект переезжает на новое место все переменные которые на него смотрят меняют значение.
value obj, p1, p2;
// PROTECT ctor:
cs_push_var_address(c, &obj);
cs_push_var_address(c, &p1);
cs_push_var_address(c, &p2);
some_allocating_function(c);
// obj, p1, p2 may have different values here due to GC
// PROTECT dtor:
cs_pop_var_addresses(c,3)
Здравствуйте, jazzer, Вы писали:
J>это все для случая, который ты показал — что все, что нужно защитить, есть в аргументах. J>Если нужно защищать еще какие-то локальные объекты на стеке, возникающие по мере выполнения функции — тогда надо их объявление упаковать в макрос, который сразу же после объявления сделает ему PROTECT.
аргументы или перменные — не важно.
Они все на стеке лежат если рассматривать идеальную "C машину". Практика показывает что не всегда на стеке, а например в регистрах.
Не сильно хочется влазить в специфику процессоров или компиляторов. Сейчас оно компилируется всем что шевелится, т.е. чистый и незамутнённый C/C++.
Но если без этого никак то придётся наверное.
В принципе я могу текущую VM* положить в TLS если это поможет. Чтобы не таскать параметр для PROTECT например.
Здравствуйте, c-smile, Вы писали:
CS>аргументы или перменные — не важно. CS>Они все на стеке лежат если рассматривать идеальную "C машину".
Не идеальную, а простую.
CS>Практика показывает что не всегда на стеке, а например в регистрах. CS>Не сильно хочется влазить в специфику процессоров или компиляторов. Сейчас оно компилируется всем что шевелится, т.е. чистый и незамутнённый C/C++. CS>Но если без этого никак то придётся наверное.
Даже если будешь некроссплатформенно сканировать регистры и стек, то проблема с консервативностью всё равно останется
CS>В принципе я могу текущую VM* положить в TLS если это поможет. Чтобы не таскать параметр для PROTECT например.
Либо через TLS, либо через быструю маску по адресу — проблема доп параметра решается. Ещё есть какие-нибудь барьеры для использования RAII обёрток?
All GC thing pointers stored on the stack (i.e., local variables and parameters to functions) must use the JS::Rooted<T> class.
...
SpiderMonkey makes it easy to remember to use JS::Rooted<T> types instead of a raw pointer because all of the API methods that may GC take a JS::Handle<T>, as described below.
...
All GC thing pointers that are parameters to a function must be wrapped in JS::Handle<T>. A JS::Handle<T> is a reference to a JS::Rooted<T>. All JS::Handle<T> are created implicitly by referencing a JS::Rooted<T>: It is not valid to create a JS::Handle<T> manually. Like JS::Rooted<T>, a JS::Handle<T> can be used as if it were the underlying pointer.
Since only a JS::Rooted<T> will cast to a JS::Handle<T>, the compiler will enforce correct rooting of any parameters passed to a function that may trigger GC. JS::Handle<T> exists because creating and destroying a JS::Rooted<T> is not free (though it only costs a few cycles). Thus, it makes more sense to only root the GC thing once and reuse it through an indirect reference. Like a reference, a JS::Handle is immutable: it can only ever refer to the JS::Rooted<T> that it was created for.
Вот релевантный код — там например строится интрузивный стек из Rooted.
Здравствуйте, Evgeny.Panasyuk, Вы писали: EP>Здравствуйте, c-smile, Вы писали: CS>>аргументы или перменные — не важно. CS>>Они все на стеке лежат если рассматривать идеальную "C машину". EP>Не идеальную, а простую. CS>>Практика показывает что не всегда на стеке, а например в регистрах. CS>>Не сильно хочется влазить в специфику процессоров или компиляторов. Сейчас оно компилируется всем что шевелится, т.е. чистый и незамутнённый C/C++. CS>>Но если без этого никак то придётся наверное. EP>Даже если будешь некроссплатформенно сканировать регистры и стек, то проблема с консервативностью всё равно останется
Согласен. EP>Либо через TLS, либо через быструю маску по адресу — проблема доп параметра решается. Ещё есть какие-нибудь барьеры для использования RAII обёрток? EP>P.S. В SpiderMonkey используются именно RAII обёртки:
RAII оно не бесплатное к сожалению. Мне не нравится что приходится создавать RAII объекты когда технически они будут действительно нужны в 0.01 % случаев.
Обычно исполнение кода GC не вызывает. Ну и потом в JS/SS много функций с переменным числом параметров. И далеко не всегда известно что там будет.
Вот
имплементация функции
static value CSF_concat(VM *c)
{
value vector = ThisVector(c);
value nvector = 0;
PROTECT(vector,nvector);
FETCH(c, vector);
int n = c->argc - 3 + 1;
if( n == 0)
return vector;
int_t d = CsVectorSize(c,vector);
int i;
int_t extra = 0;
for( i = 3; i <= c->argc; ++i )
{
value t = CsGetArg(c,i);
if( CsVectorP(t) )
extra += CsVectorSize(c,t);
else
extra ++;
}
nvector = CsMakeVector(c,d+extra, CsVectorClass(vector));
n = d;
for( i = 0; i < d; ++i )
CsSetVectorElement(c,nvector, i, CsVectorElement(c,vector,i));
for( i = 3; i <= c->argc; ++i )
{
value t = CsGetArg(c,i);
if( CsVectorP(t) )
{
for(int j = 0; j < CsVectorSize(c,t); ++j)
CsSetVectorElement(c,nvector,n++,CsVectorElement(c,t,j));
}
else
CsSetVectorElement(c,nvector,n++,t);
}
return nvector;
}
Здравствуйте, c-smile, Вы писали:
CS>есть имплментация script virtual machine. CS>Эта VM использует moving garbage collector, т.е. объекты гуляют по памяти когда происходит GC.
Получается, что есть три вида указателей:
— управляемые перемещаемые (за которыми мусорщик следит, потому что знает о них)
— прибитые гвоздями (о которых мусорщик тоже знает)
— копии управляемых (о которых мусорщик не знает, ради оптимизации)
Решение номер раз: избавиться от копий. Это значит, что передавать указатели по ссылке.
Тут вопрос, что дешевле — держать мусорщик в курсе обо всём, или двойное разыменование. Думаю, что двойное разыменование дешевле.
void foo(VM* vm, managed_pointer const& p1, managed_pointer const& p2) {
// если хочется работать с локальной копией - создаём и знакомим мусорщик с ней
managed_pointer q1, q3, q4;
BEGIN_TRACK(vm, &q1, &q3, &q4); // не залочили, а запомнили, что на стеке есть такая штука
// пару BEGIN_TRACK - END_TRACK можно засахарить в раии SCOPED_TRACK
q1 = p1;
...
assert(p1 == q1); // мусорщик должен синхронно обновлять значения (адреса объектов) подконтрольных указателей
...
bar(vm, p2);
...
bar(vm, p2);
...
END_TRACK(vm, &q1, &q3, &q4);
}
Номер два: чтобы случайно не передать указатель по значению, — использовать прокси-ссылку. Везде. Цена этого — слежение в уме за временем жизни ссылаемого указателя. Поэтому совет может быть как полезным, так и вредным.
Номер три: использовать на стеке только scoped-переменные. И вообще запретить любую работу с примитивными managed_pointer-ами, кроме как передавать их по ссылке.
void foo(VM* vm, managed_pointer const& p1, managed_pointer const& p2) {
scoped_managed_pointer q1(vm), q3(vm); // которые нельзя просто так сконструировать - только через знакомство с мусорщиком
scoped_managed_pointer q2(vm, p2);
q1 = p1;
}
Цена вопроса — наивная реализация потребует для N переменных запомнить N раз указатель на мусорщик.
Номер четыре: сделать scoped синтаксическим сахаром. Грубо говоря,
Здравствуйте, Кодт, Вы писали:
К>чтобы случайно не передать указатель по значению, — использовать прокси-ссылку. Везде. Цена этого — слежение в уме за временем жизни ссылаемого указателя. Поэтому совет может быть как полезным, так и вредным. К>
All GC thing pointers that are parameters to a function must be wrapped in JS::Handle<T>. A JS::Handle<T> is a reference to a JS::Rooted<T>. All JS::Handle<T> are created implicitly by referencing a JS::Rooted<T>: It is not valid to create a JS::Handle<T> manually. Like JS::Rooted<T>, a JS::Handle<T> can be used as if it were the underlying pointer.
Since only a JS::Rooted<T> will cast to a JS::Handle<T>, the compiler will enforce correct rooting of any parameters passed to a function that may trigger GC. JS::Handle<T> exists because creating and destroying a JS::Rooted<T> is not free (though it only costs a few cycles). Thus, it makes more sense to only root the GC thing once and reuse it through an indirect reference. Like a reference, a JS::Handle is immutable: it can only ever refer to the JS::Rooted<T> that it was created for.
Здравствуйте, c-smile, Вы писали:
EP>>Либо через TLS, либо через быструю маску по адресу — проблема доп параметра решается. Ещё есть какие-нибудь барьеры для использования RAII обёрток? EP>>P.S. В SpiderMonkey используются именно RAII обёртки: CS>RAII оно не бесплатное к сожалению.
Ты и так уже используешь PROTECT, у которого внутри то же RAII.
Есть вариант заменить PROTECT на Rooted<T>, как в SpiderMonkey, что избавит от проблем с PROTECT которые ты описал.
CS>Мне не нравится что приходится создавать RAII объекты когда технически они будут действительно нужны в 0.01 % случаев.
Понимаю и разделяю эту тоску Если нужен точный перемещающий GC, то альтернативой явной регистрации указателей будет модификация компилятора (например генерация карт кадров стека для каждой функции, по типу как с исключениями).
CS>Ну и потом в JS/SS много функций с переменным числом параметров. И далеко не всегда известно что там будет. CS>array.concat(p1,p2,p3, ...) CS>там вообще параметров нет, берутся непосредственно со стека VM.
Не пойму в чём проблема — любой указатель возвращаемый из VM сразу оборачивай в специальный smart_ptr, вот прямо в API самой VM.
CS>Эх, что-то мне говорит что придется так и жить с этим PROTECT ... Нет шастя в жизни ...
В SpiderMonkey живут без PROTECT, со специальными smart_ptr JS::Rooted<T>, что проще и надёжней.