Межпроцедурная оптимизация

Эта статья находится на начальном уровне проработки, в одной из её версий выборочно используется текст из источника, распространяемого под свободной лицензией
Материал из энциклопедии Руниверсалис

Межпроцедурная оптимизация (англ. Interprocedural Optimization, IPO), или полнопрограммная оптимизация программ (англ. whole program optimization) — оптимизация компилятора, которая использует глобальный анализ потока управления и затрагивает множество процедур, даже находящихся в разных модулях, за счёт чего может достигаться существенный прирост быстродействия.

С ростом объёма программ разработчики стали делать свой код всё более удобочитаемым и повторно используемым. Зачастую это приводит к тому, что процедуры становятся предельно общими, в то время как в конкретной программе можно обойтись и частным случаем. Задача межпроцедурной оптимизации — именно генерация таких частных случаев.

Межпроцедурная оптимизация выполняется компилятором автоматически (иногда с указанием специальных директив). Её активация может приводить к существенному увеличению времени компиляции. К компиляторам, умеющим выполнять указанную оптимизацию, относятся MLton и MLKit для Standard ML, Stalin[англ.] для Scheme, JHC для Haskell, Intel C++ Compiler.

Примеры

Замена параметра функции константой

Пройдя по коду, компилятор убеждается, что один из параметров всегда константа, и уничтожает его.

Было

void DoSomething(Object* aObj, int aParam)
{
  if (aObj==NULL)
    throw logic_error("aObj==NULL");
  cout << "DoSomething(" << aObj->name() << "," << aParam << ")" << endl;
}

int main()
{
  Object obj1, obj2;

  DoSomething(&obj1, 1);
  DoSomething(&obj2, 1);

  return 0;
}

Стало

void DoSomething(Object* aObj)
{
  if (aObj==NULL)
    throw logic_error("aObj==NULL");
  cout << "DoSomething(" << aObj->name() << "," << 1 << ")" << endl;
}

int main()
{
  Object obj1, obj2;

  DoSomething(&obj1);
  DoSomething(&obj2);

  return 0;
}

Замена виртуального вызова статическим

Здесь компилятор убеждается, что все реально выполняющиеся виртуальные вызовы ведут к вызову одной и той же функции. Вместо того, чтобы обращаться к таблице виртуальных методов, компилятор делает прямой вызов функции.

В том же примере, если Object::name() — виртуальный метод, оптимизированная функция будет выглядеть так.

void DoSomething(Object* aObj)
{
  if (aObj==NULL)
    throw logic_error("aObj==NULL");
  cout << "DoSomething(" << Object::name(aObj) << "," << 1 << ")" << endl;
}

Удаление незадействованного кода

После удаления получится:

void DoSomething(Object* aObj)
{
  cout << "DoSomething(" << Object::name(aObj) << "," << 1 << ")" << endl;
}

Заодно может происходить чистка таблиц виртуальных методов.

Инлайнинг

Если функция используется однократно, она напрямую включается в то место, из которого она вызывается.

Небольшие функции также можно напрямую включать в вызывающий код.

Многие языки программирования (Паскаль, Java, D) не имеют ключевого слова inline, и решение инлайнировать функцию принимается оптимизатором (в случае Java — обфускатором).

Было

inline int DoSomething(int aParam)
{
  return aParam * aParam;
}

int main()
{
  int x = 2;
  int y = 3;
  
  cout << x << "^2=" << DoSomething(x) << ", " << y << "^2=" << DoSomething(y) << endl;

  return 0;
}

Стало

int main()
{
  int x = 2;
  int y = 3;
  
  cout << x << "^2=" << x*x << ", " << y << "^2=" << y*y << endl;

  return 0;
}

Ссылки

  • Thomas C. Spillman, «Exposing side effects in a PL/I optimizing compiler», in Proceedings of IFIPS 1971, North-Holland Publishing Company, pages 376—381.
  • Frances E. Allen, «Interprocedural Data Flow Analysis», IFIPS Proceedings, 1974.
  • Frances E. Allen, and Jack Schwartz, «Determining the Data Flow Relationships in a Collection of Procedures», IBM Research Report RC 4989, Aug. 1974.
  • Philip Abrams, «An APL Machine», Stanford University Computer Science Department, Report STAN-CS-70-158, February, 1970.
  • Terrence C. Miller, «Tentative Compilation: A Design for an APL Compiler», Ph.D. Thesis, Yale University, 1978.