От: | VladD2 | www.nemerle.org | |
Дата: | 30.10.05 23:43 | ||
Оценка: | 14 (3) +1 |
Выделение памяти в стеке
C++ дает программистам выбор между размещением объектов в куче или в стеке. Размещение в стеке эффективнее: выделение памяти дешевле, освобождение совсем ничего не стоит, а язык предоставляет поддержку при разметке сроков жизни объектов, снижая риск забыть освободить объект. С другой стороны, в C++ нужно быть очень внимательным при публикации или совместном использовании ссылок на размещенные в стеке объекты, поскольку они автоматически освобождаются при раскрутке фрейма стека, что приводит к появлению "висячих" указателей.
Еще одно достоинство размещения в стеке состоит в том, что оно куда лучше работает с кешем. На современных процессорах стоимость промаха мимо кеша весьма существенна, так что если язык и runtime могут помочь программе достичь лучшей локальности данных, производительность также повысится. Вершина стека в кеше практически всегда "горячая", а вершина кучи – "холодная" (поскольку с момента последнего использования памяти, скорее всего, прошло немало времени). В результате размещение объекта в куче скорее приведет к большему числу промахов мимо Кеша, чем размещение объекта в стеке.
Что еще хуже, промах мимо кеша при размещении объекта в куче приводит к весьма грязной работе с памятью. При выделении памяти из кучи содержание этой памяти является мусором – битами, оставшимися с момента последнего использования памяти. При выделении не хранящегося в кеше блока памяти из кучи, исполнение приостановится на время переноса содержимого этой памяти в кеш. Затем эти значения будут немедленно переписаны нулями или другими исходными значениями. Все это вместе означает большой объем напрасной работы с памятью (некоторые процессоры, например, Azul Vega, аппаратно ускоряют выделение памяти из кучи).
Escape-анализ
Язык Java не предлагает никакого способа явно разместить объект в стеке, но это не мешает JVM при случае использовать размещение в стеке. JVM могут использовать технику, именуемую escape-анализом (escape analysis), который может определить, что определенные объекты остаются прикованными к определенному потоку на весь срок жизни, и что этот срок жизни ограничен сроком жизни данного фрейма стека. Такие объекты можно безопасно размещать в стеке вместо кучи. Даже лучше, в случае мелких объектов, JVM может полностью избавиться от выделения памяти и просто размещать поля объектов в регистрах.
Листинг 2 показывает пример применения escape-анализа. Метод Component.getLocation() создает защитную копию поля location, так что вызывающая сторона не может случайно изменить текущее расположение компонента. Вызов getDistanceFrom() сперва получает местоположение другого компонента (что включает создание объекта), а затем использует поля x и y объекта, возвращаемого getLocation(), для вычисления расстояния между двумя экземплярами компонентов.
Листинг 2.
public class Point { private int x, y; public Point(int x, int y) { this.x = x; this.y = y; } public Point(Point p) { this(p.x, p.y); } public int getX() { return x; } public int getY() { return y; } public void setX(int newValue) { x = newValue; } public void setY(int newValue) { y = newValue; } } public class Component { private Point location; public Point getLocation() { return new Point(location); } public double getDistanceFrom(Component other) { Point otherLocation = other.getLocation(); int deltaX = otherLocation.getX() - location.getX(); int deltaY = otherLocation.getY() - location.getY(); return Math.sqrt(deltaX*deltaX + deltaY*deltaY); } }
Метод getLocation() не знает, что вызывающая сторона собирается делать с возвращаемым им Point; она может удерживать ссылку на него, например, поместив ее в коллекцию, так что getLocation() написан в безопасной манере. Но в этом примере getDistanceFrom() не собирается модифицировать получаемые объекты или запоминать ссылки на них. Он просто собирается ненадолго задействовать Point, а затем избавиться от него, что в данном случае выглядит как разбазаривание ресурсов.
Хорошая JVM может разобраться в происходящем и избавиться от размещения защитной копии. Сначала вместо вызова getLocation() будет подставлено (inlined) тело этого метода, то же будет сделано с вызовами getX() и getY(), в результате чего getDistanceFrom() станет выглядеть так, как показано в листинге 3.
Листинг 3.
public double getDistanceFrom(Component other) { Point otherLocation = new Point(other.x, other.y); int deltaX = otherLocation.x - location.x; int deltaY = otherLocation.y - location.y; return Math.sqrt(deltaX*deltaX + deltaY*deltaY); }
Здесь escape-анализ может показать, что объект, создаваемый в первой строке, никогда не покинет своего базового блока и что getDistanceFrom() никогда не изменяет состояние компонента other (под "покинет" имеется в виду то, что ссылка на него не хранится в куче и не передается неизвестному коду, который может удерживать копию). При том, что Point действительно используется только в одном потоке и его время жизни, как известно, ограничено базовым блоком, в котором он размещен, он может быть либо размещен в стеке, либо полностью соптимизирован, как показано в листинге 4.
Листинг 4.
public double getDistanceFrom(Component other) { int tempX = other.x, tempY = other.y; int deltaX = tempX - location.x; int deltaY = tempY - location.y; return Math.sqrt(deltaX*deltaX + deltaY*deltaY); }
В результате мы получаем точно такую же производительность, как если бы все поля были публичными, но с сохранением безопасности, предоставляемой инкапсуляцией и защитным копированием (и остальными техниками безопасного программирования).
Escape-анализ в Mustang
Escape-анализ – это оптимизация, о которой говорилось уже давно, и вот, наконец, она здесь – текущие версии Mustang (Java SE 6) могут выполнять escape-анализ и конвертировать выделение памяти в куче в выделение памяти в стеке (или просто избавляться от выделения памяти) там, где это возможно. Использование escape-анализа для устранения некоторых распределений памяти приводит к еще большему ускорению работы с памятью, снижению расхода памяти и уменьшению количества промахов мимо кеша. Кроме того, это снижает нагрузку на сборщик мусора и позволяет реже производить сборку.
Заключение
JVM удивительно хорошо делают вещи, которые раньше были полностью отданы на откуп разработчику. Позволяя JVM выбирать между выделением памяти в куче и в стеке, можно получить преимущества производительности выделения памяти в стеке, не заставляя программиста мучаться с выбором.
Brian Goetz