Linux и Windows используют разные стратегии для доступа к данным, хранящимся в динамических библиотеках.
В Linux неопределенная ссылка на объект преобразуется в библиотеку во время ссылки. Компоновщик находит размер объекта и резервирует место для него в сегменте .bss
или .rdata
исполняемого файла. При выполнении динамический компоновщик (ld.so
) разрешает символ в динамическую библиотеку (снова) и копирует объект из динамической библиотеки в память процесса.
В Windows неопределенная ссылка на объект преобразуется в библиотеку импорта во время ссылки, и для него не зарезервировано пространство. Когда модуль выполняется, динамический компоновщик преобразует символ в динамическую библиотеку и создает в процессе копию карты памяти записи в процессе, поддерживаемую общим сегментом данных в динамической библиотеке.
Преимущество копии на карте памяти записи состоит в том, что если связанные данные не изменяются, то они могут использоваться другими процессами. На практике это незначительное преимущество, которое значительно увеличивает сложность как для цепочки инструментов, так и для программ, использующих динамические библиотеки. Для объектов, которые действительно написаны, это всегда менее эффективно.
Я подозреваю, хотя у меня нет доказательств, что это решение было принято для конкретного и в настоящее время устаревшего варианта использования. Возможно, было обычной практикой использовать большие (пока что) объекты только для чтения в динамических библиотеках в 16-битной Windows (в официальных программах Microsoft или иным образом). В любом случае, я сомневаюсь, что у кого-то в Microsoft есть опыт и время, чтобы изменить это сейчас.
Чтобы исследовать проблему, я создал программу, которая пишет в объект из динамической библиотеки. Он записывает один объект на страницу (4096 байтов) в объекте, затем записывает весь объект, а затем повторяет начальный один байт на страницу записи. Если объект зарезервирован для процесса до вызова main
, первый и третий циклы должны занять примерно одинаковое время, а второй цикл должен занять больше времени, чем оба. Если объект является копией при записи на карту в динамическую библиотеку, первый цикл должен занимать по меньшей мере столько же времени, сколько второй, а третий должен занимать меньше времени, чем оба.
Результаты согласуются с моей гипотезой, и анализ разборки подтверждает, что Linux обращается к данным динамической библиотеки по адресу времени ссылки относительно счетчика программы. Удивительно, но Windows не только косвенно обращается к данным, указатель на данные и их длина перезагружаются из таблицы адресов импорта при каждой итерации цикла с включенной оптимизацией . Это было протестировано в Visual Studio 2010 на Windows XP, поэтому, возможно, все изменилось, хотя я бы не подумал, что это изменилось.
Вот результаты для Linux:
$ dd bs=1M count=16 if=/dev/urandom of=libdat.dat
$ xxd -i libdat.dat libdat.c
$ gcc -O3 -g -shared -fPIC libdat.c -o libdat.so
$ gcc -O3 -g -no-pie -L. -ldat dat.c -o dat
$ LD_LIBRARY_PATH=. ./dat
local = 0x1601060
libdat_dat = 0x601040
libdat_dat_len = 0x601020
dirty= 461us write= 12184us retry= 456us
$ nm dat
[...]
0000000000601040 B libdat_dat
0000000000601020 B libdat_dat_len
0000000001601060 B local
[...]
$ objdump -d -j.text dat
[...]
400693: 8b 35 87 09 20 00 mov 0x200987(%rip),%esi # 601020 <libdat_dat_len>
[...]
4006a3: 31 c0 xor %eax,%eax # zero loop counter
4006a5: 48 8d 15 94 09 20 00 lea 0x200994(%rip),%rdx # 601040 <libdat_dat>
4006ac: 0f 1f 40 00 nopl 0x0(%rax) # align loop for efficiency
4006b0: 89 c1 mov %eax,%ecx # store data offset in ecx
4006b2: 05 00 10 00 00 add $0x1000,%eax # add PAGESIZE to data offset
4006b7: c6 04 0a 00 movb $0x0,(%rdx,%rcx,1) # write a zero byte to data
4006bb: 39 f0 cmp %esi,%eax # test loop condition
4006bd: 72 f1 jb 4006b0 <main+0x30> # continue loop if data is left
[...]
Вот результаты для Windows:
$ cl /Ox /Zi /LD libdat.c /link /EXPORT:libdat_dat /EXPORT:libdat_dat_len
[...]
$ cl /Ox /Zi dat.c libdat.lib
[...]
$ dat.exe # note low resolution timer means retry is too small to measure
local = 0041EEA0
libdat_dat = 1000E000
libdat_dat_len = 1100E000
dirty= 20312us write= 3125us retry= 0us
$ dumpbin /symbols dat.exe
[...]
9000 .data
1000 .idata
5000 .rdata
1000 .reloc
17000 .text
[...]
$ dumpbin /disasm dat.exe
[...]
004010BA: 33 C0 xor eax,eax # zero loop counter
[...]
004010C0: 8B 15 8C 63 42 00 mov edx,dword ptr [__imp__libdat_dat] # store data pointer in edx
004010C6: C6 04 02 00 mov byte ptr [edx+eax],0 # write a zero byte to data
004010CA: 8B 0D 88 63 42 00 mov ecx,dword ptr [__imp__libdat_dat_len] # store data length in ecx
004010D0: 05 00 10 00 00 add eax,1000h # add PAGESIZE to data offset
004010D5: 3B 01 cmp eax,dword ptr [ecx] # test loop condition
004010D7: 72 E7 jb 004010C0 # continue loop if data is left
[...]
Вот исходный код, использованный для обоих тестов:
#include <stdio.h>
#ifdef _WIN32
#include <windows.h>
typedef FILETIME time_l;
time_l time_get(void) {
FILETIME ret; GetSystemTimeAsFileTime(&ret); return ret;
}
long long int time_diff(time_l const *c1, time_l const *c2) {
return 1LL*c2->dwLowDateTime/100-c1->dwLowDateTime/100+c2->dwHighDateTime*100000-c1->dwHighDateTime*100000;
}
#else
#include <unistd.h>
#include <time.h>
#include <stdlib.h>
typedef struct timespec time_l;
time_l time_get(void) {
time_l ret; clock_gettime(CLOCK_MONOTONIC, &ret); return ret;
}
long long int time_diff(time_l const *c1, time_l const *c2) {
return 1LL*c2->tv_nsec/1000-c1->tv_nsec/1000+c2->tv_sec*1000000-c1->tv_sec*1000000;
}
#endif
#ifndef PAGESIZE
#define PAGESIZE 4096
#endif
#ifdef _WIN32
#define DLLIMPORT __declspec(dllimport)
#else
#define DLLIMPORT
#endif
extern DLLIMPORT unsigned char volatile libdat_dat[];
extern DLLIMPORT unsigned int libdat_dat_len;
unsigned int local[4096];
int main(void) {
unsigned int i;
time_l t1, t2, t3, t4;
long long int d1, d2, d3;
t1 = time_get();
for(i=0; i < libdat_dat_len; i+=PAGESIZE) {
libdat_dat[i] = 0;
}
t2 = time_get();
for(i=0; i < libdat_dat_len; i++) {
libdat_dat[i] = 0xFF;
}
t3 = time_get();
for(i=0; i < libdat_dat_len; i+=PAGESIZE) {
libdat_dat[i] = 0;
}
t4 = time_get();
d1 = time_diff(&t1, &t2);
d2 = time_diff(&t2, &t3);
d3 = time_diff(&t3, &t4);
printf("%-15s= %18p\n%-15s= %18p\n%-15s= %18p\n", "local", local, "libdat_dat", libdat_dat, "libdat_dat_len", &libdat_dat_len);
printf("dirty=%9lldus write=%9lldus retry=%9lldus\n", d1, d2, d3);
return 0;
}
Я искренне надеюсь, что кто-то еще выиграет от моего исследования. Спасибо за чтение!