Предистория
При работе с неуправляемым кодом очень важно понимать как нельзя передавать данные. Особенно считая что я использую всё чтобы уменьшить траты времени на вызов неуправляемого кода до предела в модулях OpenCL
и OpenGL
.
Но долгое время я основывался на странном поведении метода System.Diagnostics.Debug.WriteLine
, который на старом Win7х64 ноуте с криво установленным .Net Framework почему-то заставлял объекты двигаться в памяти. И только 1 раз после запуска программы.
Ну, у меня были причины верить результатам что я получил с ним, потому что я исключил все остальные возможности что мог придумать. Но ещё тогда я не мог воспроизвести этот же эффект на других машинах, поэтому всё равно была вероятность что я тестирую поведение лично своей среды .Net, а не любой.
Но сейчас у меня дошли руки нормально разобраться в этом. И я даже нашёл способ добиться того же поведения, но от вызова GC.Collect
напрямую. Способ всё ещё костыльный, но для начала надо понять подводные камни хотя бы этого способа.
Пожалуйста, попробуйте у себя
Вот архив с тестом:
1.rar (190.8 КБ)
В архиве сразу запакована C++ библиотека, в которой важна всего одна функция:
extern "C" __declspec(dllexport) BYTE * copy_arr(int* a, void (*punch_gc)(void*))
{
BYTE* res = new BYTE[20]; // Выделяем 20 байт неуправляемой памяти
punch_gc(a); // Вызываем ту подпрограмму, чей адрес сюда передали
memcpy(res, a, 20); // Копируем 20 байт из "a" в "res"
return res; // Плохо что неуправляемая память не освобождается, но в этом тесте не важно
}
Библиотека уже откомпилирована и .pas должен работать из коробки, но желательно попробовать без и с перекомпиляцией этой библиотеки у вас, если есть студия с C++ пакетом.
Тесты находятся в единственном .pas файле и состоят в следующем:
Есть два массива, a
и b
.
Разные тесты пытаются по-разному передавать массив a
неуправляемому коду.
Тем временем массив b
просто лежит. Он нужен только чтобы видеть когда что-то да сдвинулось, если массив a
был хорошо заблокирован.
Затем из неуправляемого кода управляемый callback пинает GC, чтобы он сдвинул управляемые объекты (в идеале какие может, но пока надеемся на шанс).
И в конце проверяется, есть ли разница между тем, какой адрес хранится в неуправляемом коде и какой реальный адрес содержимого массива после пинания GC.
Для тестирования надо запускать файл 1.pas из корня архива, раскомметируя одну из строк из региона заголовки вызова copy_arr
.
Как понимать вывод:
К примеру, если раскомметировать третий заголовок из группы безопасных:
var ptr := copy_arr(a[1],
Вывод будет чем то типа:
begin
$307D500
$307D560
before gc
$307D500
$307D504
$307D560
after gc
$307D500
$307D504
$3017E70
end
$307D500
$3017E70
2 0 0 0 3 0 0 0 4 0 0 0 5 0 0 0 6 0 0 0
Главные разделы на которые надо смотреть это before gc
и after gc
. Оба устроены одинаково, 3 числа значат:
- Реальный адрес содержимого массива
a
. - Адрес, который хранится в неуправляемом коде.
- Реальный адрес содержимого массива
b
.
Изначально адрес содержимого a
, хранимый в неуправляемом коде был на 4 больше реального. Это правильно, потому что заголовок передаёт адрес [1]
элемента массива, и каждый элемент занимает по 4 байта.
После пинка GC содержимое a
осталось там где было, а содержимое b
переместилось с $307D560
на $3017E70
. То есть сборка мусора произошла и сдвинула объекты, но массив a
остался заблокировать в памяти.
Желательно запускать несколько раз, потому что, как написано выше - пинок GC всё ещё надеется на очень плавающий шанс.
Другой пример - первый из не безопасных заголовков:
var ptr := copy_arr(Marshal.UnsafeAddrOfPinnedArrayElement(a,0),
Вывод:
begin
$2A5D500
$2A5D560
before gc
$2A5D500
$2A5D500
$2A5D560
after gc
$29F7E70
$2A5D500
$29F7ED0
end
$29F7E70
$29F7ED0
1 0 0 0 2 0 0 0 3 0 0 0 4 0 0 0 5 0 0 0
В этом случае посылали адрес начала содержимого a
: $2A5D500
. После пинка GC реальный адрес поменялся на $29F7E70
, но неуправляемый код всё ещё держит старый адрес.
Тут смотреть на b
уже нет смысла - состояние массива a
уже доказало что метод начинающийся с Marshal.Unsafe
таки является не безопасным.
TL;DR
Чтобы протестировать и подтвердить что поведение GC
у вас такое же как у меня - надо запустить файл 1.pas
из корня архива, пробуя расскомметировать каждую 1 строчку из региона заголовки вызова copy_arr
. С каждой из этих строчек запускать несколько раз по следующему принципу:
Если расскоментирована строчка после комментария // Безопастно
, первые числа после before gc
и после after gc
в выводе должны быть одинаковыми, но при этом третьи числа там же должны быть разными.
Тест успешно пройден если при нескольких запусках теста получили разные третьи числа, но при этом ни в при одном из запусков не получили разные первые числа.
Если расскоментирована строчка после комментария // НЕ безопастно
, первые числа после before gc
и после after gc
в выводе должно быть разными.
Тест успешно пройден если хотя бы при одном из запусков теста получили разные первые числа.