Снижение производительности при выполнении инструкций x86, хранящихся в сегменте данных? - PullRequest
5 голосов
/ 22 октября 2009

У меня есть простая программа, которая сначала записывает некоторые собственные инструкции x86 в объявленный буфер, а затем устанавливает указатель функции на этот буфер и выполняет вызов. Однако я замечаю серьезное снижение производительности, когда этот буфер выделяется в стеке (а не в куче или даже в области глобальных данных). Я проверил, что начало последовательности команд в буфере данных находится на 16-байтовой границе (я предполагаю, что это то, что процессор требует (или хочет), чтобы это было). Я не знаю, почему будет иметь значение, где я выполняю свои инструкции в процессе, но в приведенной ниже программе «ХОРОШО» выполняется на моей двухъядерной рабочей станции за 4 секунды, а «ПЛОХО» - около 6 минут , Есть ли здесь какая-то проблема с выравниванием / i-cache / предсказанием? Моя оценочная лицензия на VTune только что закончилась, поэтому я даже не могу провести анализ по этому вопросу :(. Спасибо.


#include <stdio.h>
#include <string.h>
#include <stdlib.h>

typedef int (*funcPtrType)(int, int);

int foo(int a, int b) { return a + b; }

void main()
{
  // Instructions in buf are identical to what the compiler generated for "foo".
  char buf[201] = {0x55,
                   0x8b, 0xec,
                   0x8b, 0x45, 0x08,
                   0x03, 0x45, 0x0c,
                   0x5D,
                   0xc3
                  };

  int i;

  funcPtrType ptr;

#ifdef GOOD
  char* heapBuf = (char*)malloc(200);
  printf("Addr of heap buf: %x\n", &heapBuf[0]);
  memcpy(heapBuf, buf, 200);
  ptr = (funcPtrType)(&heapBuf[0]);
#else // BAD
  printf("Addr of local buf: %x\n", &buf[0]);
  ptr = (funcPtrType)(&buf[0]);
#endif

  for (i=0; i < 1000000000; i++)
    ptr(1,2);
}

Результаты выполнения этого:

$ cl -DGOOD ne3.cpp
32-разрядный оптимизирующий компилятор C / C ++ Microsoft (R) версии 11.00.7022 для 80x86
Copyright (C) Microsoft Corp 1984-1997. Все права защищены.

ne3.cpp
32-разрядный инкрементальный компоновщик Microsoft (R) версии 5.10.7303
Авторские права (C) Microsoft Corp 1992-1997. Все права защищены.

/ выход: ne3.exe
ne3.obj
$ time ./ne3
Адрес кучи buf: 410eb0

реальный 0 м 4,33 с
пользователь 0m 4.31s
sys 0m 0,01 с
$
$
$ cl ne3.cpp
32-разрядный оптимизирующий компилятор C / C ++ Microsoft (R) версии 11.00.7022 для 80x86
Copyright (C) Microsoft Corp 1984-1997. Все права защищены.

ne3.cpp
32-разрядный инкрементальный компоновщик Microsoft (R) версии 5.10.7303
Авторские права (C) Microsoft Corp 1992-1997. Все права защищены.

/ выход: ne3.exe
ne3.obj
$ time ./ne3
Адрес локального буфера: 12feb0

реально 6м41.19с
пользователь 6m40.46s
sys 0m 0,03 с
$

Спасибо.

  • Shasank

Ответы [ 2 ]

3 голосов
/ 22 октября 2009

Защита стека для безопасности?

Как дикое предположение, вы можете столкнуться со схемой защиты стека на основе MMU. Ряд дыр в безопасности был основан на преднамеренных переполнениях буфера, которые вводят исполняемый код в стек. Один из способов борьбы с этим - неисполняемый стек. Это может привести к ловушке в ОС, где, я полагаю, возможно, что ОС или какой-нибудь вирусный программный продукт что-то сделает.

Отрицательное взаимодействие когерентности i-cache?

Другая возможность состоит в том, что использование доступа к коду и данным к близлежащим адресам наносит ущерб стратегии кэширования ЦП. Я полагаю, что x86 реализует по существу автоматическую модель когерентности кода / данных, которая может привести к аннулированию большого количества соседних кэшированных инструкций при любой записи в память. Вы не можете исправить это, изменив свою программу так, чтобы она не использовала стек (очевидно, вы можете перемещать динамический код), потому что стек записывается машинным кодом все время, например, когда для параметра или адреса возврата выдвигается значение вызов процедуры.

В наши дни процессоры действительно быстры по сравнению с DRAM или даже кольцами кэша внешнего уровня, поэтому все, что нарушает кольца внутреннего кэша, будет довольно серьезным, плюс его реализация, вероятно, предполагает своего рода микроловушку в реализации CPU сопровождаемый "петлей" в HW, чтобы сделать недействительными вещи. Это не то, что Intel или AMD беспокоились бы о скорости, так как для большинства программ это никогда не происходило, а когда это происходило, обычно это происходило только один раз после загрузки программы.

2 голосов
/ 22 октября 2009

Я предполагаю, что, поскольку у вас есть переменная i в стеке, и когда вы изменяете i в цикле for, вы удаляете ту же строку кэша, в которой находится код. Поместите код в середине вашего буфера где-то (и, возможно, увеличить буфер), чтобы отделить его от других переменных стека.

Также обратите внимание, что выполнение инструкций в стеке обычно является отличительной чертой эксплойта безопасности (такого как переполнение буфера).

Поэтому ОС часто настроена так, чтобы запретить это поведение. Вирусные сканеры также могут принять меры против этого. Возможно, ваша программа проходит проверку безопасности каждый раз, когда она пытается получить доступ к этой странице стека (хотя я ожидаю, что в этом случае поле времени sys будет больше).

Если вы хотите «официально» сделать страницу памяти исполняемой, вам, вероятно, стоит заглянуть в VirtualProtect().

...