Русский
Русский
English
Статистика
Реклама

Jit-компилятор

PHP 8 Что нового?

25.11.2020 16:04:07 | Автор: admin

PHP, начиная с 7 версии, кардинально изменился. Код стал куда быстрее и надёжнее, и писать его стало намного приятнее. Но вот, уже релиз 8 версии! Ноябрь 26, 2020 примерно на год раньше, чем обещали сами разработчики. И всё же, не смотря на это, мажорная версия получилась особенно удачной. В этой статье я попытаюсь выложить основные приятные изменения, которые мы должны знать.


1. JIT


Как говорят сами разработчики, они выжали максимум производительности в 7 версии (тем самым сделав PHP наиболее шустрым среди динамических ЯПов). Для подальшего ускорения, без JIT-компилятора не обойтись. Справедливости ради, стоит сказать, что для веб-приложений использование JIT не сильно улучшает скорость обработки запросов (в некоторых случаях скорость будет даже меньше, чем без него). А вот, где нужно выполнять много математических операций там прирост скорости очень даже значительный. Например, теперь можно делать такие безумные вещи, как ИИ на PHP.
Включить JIT можно в настройках opcache в файле php.ini.
Подробнее 1 | Подробнее 2 | Подробнее 3


2. Аннотации/Атрибуты (Attributes)


Все мы помним, как раньше на Symfony код писался на языке комментариев. Очень радует, что такое теперь прекратится, и можно будет использовать подсказки любимой IDE, функция "Find usages", и даже рефакторинг!


Забавно, что символ # также можно было использовать для создания комментариев. Так что ничего не меняется в этом мире.

Было очень много споров о синтаксисе для атрибутов, но приняли Rust-like синтаксис:


#[ORM\Entity]#[ORM\Table("user")]class User{    #[ORM\Id, ORM\Column("integer"), ORM\GeneratedValue]    private $id;    #[ORM\Column("string", ORM\Column::UNIQUE)]    #[Assert\Email(["message" => "The email '{{ value }}' is not a valid email."])]    private $email;}

Подробнее 1 | Атрибуты в Symfony


3. Именованые параметры (Named Arguments)


Если вы когда-либо видели код, где есть булевы параметры, то понимаете насколько он ужасен. Ещё хуже, когда этих параметров 8 штук, 6 из которых имеют значение по-умолчанию, а вам нужно изменить значение последнего параметра.


К примеру, код для использования библиотеки phpamqplib:


$channel->queue_declare($queue, false, true, false, false);// ...$channel->basic_consume($queue, '', false, false, false, false, [$this, 'consume']);

С использованием именованых параметров, код становится намного легче читать:


$channel->queue_declare($queue, durable: true, auto_delete: false);// ...$channel->basic_consume($queue, callback: [$this, 'consume']);

Ещё несколько примеров:


htmlspecialchars($string, default, default, false);// vshtmlspecialchars($string, double_encode: false);

Внимание! Можно также использовать ассоциативные массивы для именованых параметров (и наоборот).


$params = ['start_index' => 0, 'num' => 100, 'value' => 50];$arr = array_fill(...$params);

function test(...$args) { var_dump($args); }test(1, 2, 3, a: 'a', b: 'b');// [1, 2, 3, "a" => "a", "b" => "b"]

Подробнее


4. Оператор безопасного null (Nullsafe operator)


Null сам по себе не очень хорошая штука (даже очень плохая). Когда функция возвращает null, то в каждом месте, где идёт её вызов, программист обязан проверить на null. И это приводит к ужасным последствиям.


$session = Session::find(123);if ($session !== null) {    $user = $session->user;    if ($user !== null) {        $address = $user->getAddress();        if ($address !== null) {            $country = $address->country;        }    }}

По хорошему, должен быть метод Session::findOrFail, который будет кидать исключение в случае отсутствия результата. Но когда эти методы диктует фреймворк, то мы не можем ничего сделать. Единственное, это проверять каждый раз на null либо, где это уместно, использовать ?->.


Да, с оператором nullsafe код станет немного лучше, но всё же это не повод возвращать null.

$country = $session?->user?->getAddress()?->country;

Этот код нельзя назвать чистым, только лишь от части. Для чистого кода, нужно использовать шаблон Null Object, либо выбрасывать exception. Идеальным вариантом было б:


$country = $session->user->getAddress()->country;

Поэтому, если возможно с вашей стороны, никогда не возвращайте null (к Римлянам 12:18).

Также интересным моментом в использовании nullsafe есть то, что при вызове метода с помощью ?->, параметры будут обработаны только если объект не null:


function expensive_function() {    var_dump('will not be executed');}$foo = null;$foo?->bar(expensive_function());

5. Оператор выбора match (Match expression v2)


Для начала покажу код до и после:


$v = 1;switch ($v) {    case 0:        $result = 'Foo';        break;    case 1:        $result = 'Bar';        break;    case 2:        $result = 'Baz';        break;}echo $result; // Bar

VS


$v = 1;echo match ($v) {    0 => 'Foo',    1 => 'Bar',    2 => 'Baz',};  // Bar

Как видим, это очень приятный оператор для выбора значений, который удобно заменяет switch.
Но есть очень важное отчилие switch от match: первый сравнивает нестрого ==, а во втором производится строгое === сравнение.


Наглядный пример различия:


switch ('foo') {    case 0:      $result = "Oh no!\n";      break;    case 'foo':      $result = "This is what I expected\n";      break;}echo $result; // Oh no!

VS


echo match ('foo') {    0 => "Oh no!\n",    'foo' => "This is what I expected\n",}; // This is what I expected

В PHP8 этот пример со switch работает по другому, далее рассмотрим это.

Также, сравниваемыми значениями оператора match могут быть выражения. При этом, будут выполнены только те, пока не будет найден первый совпадающий вариант:


$result = match ($x) {    foo() => ...,    $this->bar() => ..., // bar() isn't called if foo() matched with $x    $this->baz => ...,    // etc.};

6. Адекватное приведение строки в число (Saner string to number comparisons)


Проблема


$validValues = ["foo", "bar", "baz"];$value = 0;var_dump(in_array($value, $validValues));// bool(true) ???

Это происходит потому, что при нестрогом == сравнении строки с числом, строка приводится к числу, то-есть, например (int)"foobar" даёт 0.


В PHP8, напротив, сравнивает строку и число как числа только если строка представляет собой число. Иначе, число будет конвертировано в строку, и будет производиться строковое сравнение.


Comparison Before After
0 == "0" true true
0 == "0.0" true true
0 == "foo" true false
0 == "" true false
42 == " 42" true true
42 == "42foo" true false

Стоит отметить, что теперь выражение 0 == "" даёт false. Если у вас из базы пришло значение пустой строки и обрабатывалось как число 0, то теперь это не будет работать. Нужно вручную приводить типы.

Эти изменения относятся ко всем операциям, которые производят нестрогое сравнение:


  • Операторы <=>, ==, !=, >, >=, <, <=.
  • Функции in_array(), array_search(), array_keys() с параметром strict: false (то-есть по-умолчанию).
  • Сотрировочные функции sort(), rsort(), asort(), arsort(), array_multisort() с флагом sort_flags: SORT_REGULAR (то-есть по-умолчанию).

Также, есть специальные значения которые при нестрогом сравнении дают true:


Expression Before After
INF == "INF" false true
-INF == "-INF" false true
NAN == "NAN" false false
INF == "1e1000" true true
-INF == "-1e1000" true true

7. Constructor Property Promotion


Изначально идея позаимствована в языка-брата Hack. Она состоит в том, чтобы упростить инициализацию полей класса в конструкторе.


Вместо прописания полей класа, параметров конструктора, инициализации полей с помощью параметров, можно просто прописать поля параметрами конструктора:


class Point {    public function __construct(        public float $x = 0.0,        public float $y = 0.0,        public float $z = 0.0,    ) {}}

Это эквивалентно:


class Point {    public float $x;    public float $y;    public float $z;    public function __construct(        float $x = 0.0,        float $y = 0.0,        float $z = 0.0,    ) {        $this->x = $x;        $this->y = $y;        $this->z = $z;    }}

С этим всё просто, так как это синтаксический сахар. Но интересный момент возникает при использовании вариативные параметры (их нельзя объявлять таким образом). Для них нужно по-старинке вручную прописать поля и установить их в конструкторе:


class Test extends FooBar {    private array $integers;    public function __construct(        private int $promotedProp,         Bar $bar,        int ...$integers,    ) {        parent::__construct($bar);        $this->integers = $integers;    }}

8. Новые функции для работы со строками (str_contains, str_starts_with, str_ends_with)


Функция str_contains проверяет, содержит ли строка $haystack строку $needle:


str_contains("abc", "a"); // truestr_contains("abc", "d"); // falsestr_contains("abc", "B"); // false // $needle is an empty stringstr_contains("abc", "");  // truestr_contains("", "");     // true

Функция str_starts_with проверяет, начинается ли строка $haystack строкой $needle:


$str = "beginningMiddleEnd";var_dump(str_starts_with($str, "beg")); // truevar_dump(str_starts_with($str, "Beg")); // false

Функция str_ends_with проверяет, кончается ли строка $haystack строкой $needle:


$str = "beginningMiddleEnd";var_dump(str_ends_with($str, "End")); // truevar_dump(str_ends_with($str, "end")); // false

Вариантов mb_str_ends_with, mb_str_starts_with, mb_str_contains нету, так как эти функции уже хорошо работают с мутльтибайтовыми символами.


На самом деле, очень приятно, что наконец-то добавили эти функции. Теперь не нужно писать каждый раз своих костылей с помощью substr, strncmp, strpos.

9. Использование ::class на объектах (Allow ::class on objects)


Раньше, чтобы получить название класса, к которому принадлежит объект, нужно было использовать get_class:


$object = new stdClass;$className = get_class($object); // "stdClass"

Теперь же, можно использовать такую же нотацию, как и ClassName::class:


$object = new stdClass;var_dump($object::class); // "stdClass"

10. Возвращаемый тип static (Static return type)


Тип static был добавлен для более явного указания, что используется позднее статическое связывание (Late Static Binding) при возвращении результата:


class Foo {    public static function createFromWhatever(...$whatever): static {        return new static(...$whatever);    }}

Также, для возвращения $this, стоит указывать static вместо self:


abstract class Bar {    public function doWhatever(): static {        // Do whatever.        return $this;    }}

11. Weak Map


Это специальная структура данных для хранения значений, ключами которых являются объекты, в основном используемая для кеширования.


Интерфейс класса выглядит следующим образом:


WeakMap implements Countable , ArrayAccess , Iterator {    public __construct ( )    public count ( ) : int    public current ( ) : mixed    public key ( ) : object    public next ( ) : void    public offsetExists ( object $object ) : bool    public offsetGet ( object $object ) : mixed    public offsetSet ( object $object , mixed $value ) : void    public offsetUnset ( object $object ) : void    public rewind ( ) : void    public valid ( ) : bool}

Особенностью есть то, что объекты, используемые как ключи, подвержены сборке мусора. Поэтому, WeakMaps особенно пригодные для долгоживущих процессов.


class FooBar {    private WeakMap $cache;    public function getSomethingWithCaching(object $obj) {        return $this->cache[$obj] ??= $this->decorated->getSomething($obj);    }    // ...}

Подробнее можно почитать в документации.


12. Убрано возможность использовать левоассоциативный оператор (Deprecate left-associative ternary operator)


Рассмотрим код:


return $a == 1 ? 'one'     : $a == 2 ? 'two'     : $a == 3 ? 'three'     : $a == 4 ? 'four'              : 'other';

Вот как он всегда работал:


$a Result
1 'four'
2 'four'
3 'four'
4 'four'

В 7.4 код как прежде, отрабатывал, но выдавался Deprecated Warning.
Теперь же, в 8 версии, код упадёт с Fatal error.


13. Изменение приоритета оператора конкатенации (Change the precedence of the concatenation operator)


Раньше, приоритет оператора конкатенации . был на равне с + и -, поэтому они исполнялись поочерёдно слева направо, что приводило к ошибкам. Теперь же, его приоритет ниже:


Expression Before Currently
echo "sum: " . $a + $b; echo ("sum: " . $a) + $b; echo "sum :" . ($a + $b);

14. Удалены краткие открывающие php теги


В каждом скрипте, где в настоящее время используется короткий <? открывающий тег, нужно будет внести изменения и использовать стандартный тег <?php.


Это не касается тега <?=, так как он, начиная с 5.4 всегда доступен.


15. Новый интерфейс Stringable


Объекты, которые реализуют метод __toString, неявно реализуют этот интерфейс. Сделано это в большей мере для гарантии типобезопасности. С приходом union-типов, можно писать string|Stringable, что буквально означает "строка" или "объект, который можно преобразовать в строку". В таком случае, объект будет преобразован в строку только когда уже не будет куда оттягивать.


interface Stringable{    public function __toString(): string;}

Рассмотрим такой код:


class A{    public function __toString(): string     {        return 'hello';    }}function acceptString(string $whatever) {    var_dump($whatever);}acceptString(123.45); // string(6) "123.45"acceptString(new A()); // string(5) "hello"

Здесь функция acceptString принимает строку, но что если нам нужно конкретно объект, что может быть преобразован в строку, а не что-либо иное. Вот тут нам поможет интерфейс Stringable:


function acceptString(Stringable $whatever) {    var_dump($whatever);    var_dump((string)$whatever);}// acceptString(123.45); /*TypeError*/acceptString(new A()); /*object(A)#1 (0) {}string(5) "hello"*/

16. Теперь throw это выражение


Примеры использования:


// This was previously not possible since arrow functions only accept a single expression while throw was a statement.$callable = fn() => throw new Exception();// $value is non-nullable.$value = $nullableValue ?? throw new InvalidArgumentException();// $value is truthy.$value = $falsableValue ?: throw new InvalidArgumentException();// $value is only set if the array is not empty.$value = !empty($array)    ? reset($array)    : throw new InvalidArgumentException();

Подробнее можно почитать здесь.


17. Стабильная сортировка


Теперь все сортировки в php стабильные. Это означает, что равные элементы будут оставаться в том же порядке, что и были до сортировки.


Сюда входят sort, rsort, usort, asort, arsort, uasort, ksort, krsort, uksort, array_multisort, а также соответствующие методы в ArrayObject.


18. Возможньсть опустить переменную исключения (non-capturing catches)


Раньше, даже если переменная исключения не использовалась в блоке catch, её всё равно нужно быто объявлять (и IDE подсвечивала ошибку, что переменная нигде не используется):


try {    changeImportantData();} catch (PermissionException $ex) {    echo "You don't have permission to do this";}

Теперь же, можно опустить переменную, если никакая дополнительная информация не нужна:


try {    changeImportantData();} catch (PermissionException) { // The intention is clear: exception details are irrelevant    echo "You don't have permission to do this";}

19. Обеспечение правильной сигнатуры магических методов (Ensure correct signatures of magic methods):


Когда были добавлены type-hints в php, оставалась возможность непавильно написать сигнатуру для магических методов.
К примеру:


class Test {    public function __isset(string $propertyName): float {        return 123.45;    }}$t = new Test();var_dump(isset($t)); // true

Теперь же, всё жёстко контролируется, и допустить ошибку сложнее.


Foo::__call(string $name, array $arguments): mixed;Foo::__callStatic(string $name, array $arguments): mixed;Foo::__clone(): void;Foo::__debugInfo(): ?array;Foo::__get(string $name): mixed;Foo::__invoke(mixed $arguments): mixed;Foo::__isset(string $name): bool;Foo::__serialize(): array;Foo::__set(string $name, mixed $value): void;Foo::__set_state(array $properties): object;Foo::__sleep(): array;Foo::__unserialize(array $data): void;Foo::__unset(string $name): void;Foo::__wakeup(): void;

20. Включить расширение json по-умолчанию (Always available JSON extension)


Так как функции для работы с json постоянно используются, и нужны чуть ли не в каждом приложении, то было принято решение включить ext-json в PHP по-умолчанию.


21. Более строгие проверки типов при для арифметических и побитовых операторов (Stricter type checks for arithmetic/bitwise operators)


Проблема, которую разработчики здесь решили предоставлена кодом ниже:


var_dump([] % [42]);

Что должен вывести этот код? Здесь непредсказуемое поведение (будет 0). Всё потому, что большинство арифметических операторов не должны применятся на массивах.


Теперь, при использовании операторов +, -, *, /, **, %, <<, >>, &, |, ^, ~, ++, -- будет вызывать исключение TypeError для операндов array, resource и object.


22. Валидация абстрактных методов в трейтах (Validation for abstract trait methods)


До восьмой версии, можно было писать что-то вроде:


trait T {    abstract public function test(int $x);}class C {    use T;    // Allowed, but shouldn't be due to invalid type.    public function test(string $x) {}}

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


trait MyTrait {    abstract private function neededByTheTrait(): string;    public function doSomething() {        return strlen($this->neededByTheTrait());    }}class TraitUser {    use MyTrait;    // This is allowed:    private function neededByTheTrait(): string { }    // This is forbidden (incorrect return type)    private function neededByTheTrait(): stdClass { }    // This is forbidden (non-static changed to static)    private static function neededByTheTrait(): string { }}

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


23. Объединения типов (Union Types 2.0)


Рассмотрим код:


class Number {    /**     * @var int|float $number     */    private $number;    /**     * @param int|float $number     */    public function setNumber($number) {        $this->number = $number;    }    /**     * @return int|float     */    public function getNumber() {        return $this->number;    }}

Здесь тип переменной $number контролируется только соглашениями программистов. На самом деле, туда может попасть что-угодно, и выйти отсюда может также что-угодно, так как проверки на тип не обеспечиваются ядром языка.


Теперь же, можно прописать тип int|float (или любой другой) явно, чтобы обеспечить корректность работы модуля:


class Number {    private int|float $number;    public function setNumber(int|float $number): void {        $this->number = $number;    }    public function getNumber(): int|float {        return $this->number;    }}

А также, код становится намного чище.
Как вы уже могли заметить, типы-объединения имеют синтаксис T1|T2|... и могут быть использованы во всех местах, где можно прописать type-hints.


Некоторые оговорки:


  • Тип void не может быть частью объединения.
  • Чтобы обозначить отсутствие результата, можно объявить "Nullable union type", который имеет следующий синтаксис: T1|T2|null.
  • Тип null не может быть использован вне объединения. Вместо него стоит использовать void.
  • Существует также псевдотип false, который по историческим причинам уже используется некоторыми функциями в php. С другой стороны, не существует тип true, так как он нигде не использовался ранее.

Типы полей класса инвариантны, и не могут быть изменены при наследовании.
А вот с методами всё немного интересней:


  1. Параметры методов можно расширить, но нельзя сузить.
  2. Возвращаемые типы можно сузить, но нельзя расширить.

Вот как это выглядит в коде:


class Test {    public function param1(int $param) {}    public function param2(int|float $param) {}    public function return1(): int|float {}    public function return2(): int {}}class Test2 extends Test {    public function param1(int|float $param) {} // Allowed: Adding extra param type    public function param2(int $param) {}       // FORBIDDEN: Removing param type    public function return1(): int {}           // Allowed: Removing return type    public function return2(): int|float {}     // FORBIDDEN: Adding extra return type}

То же самое происходит при типах, которые получились как результат наследования:


class A {}class B extends A {}class Test {    public function param1(B|string $param) {}    public function param2(A|string $param) {}    public function return1(): A|string {}    public function return2(): B|string {}}class Test2 extends Test {    public function param1(A|string $param) {} // Allowed: Widening union member B -> A    public function param2(B|string $param) {} // FORBIDDEN: Restricting union member A -> B    public function return1(): B|string {}     // Allowed: Restricting union member A -> B    public function return2(): A|string {}     // FORBIDDEN: Widening union member B -> A}

Интереснее становится когда strict_types установлен в 0, то-есть по-умолчанию. Например, функция принимает int|string, а мы передали ей bool. Что в результате должно быть в переменной? Пустая строка, или ноль? Есть набор правил, по которым будет производиться приведение типов.


Так, если переданный тип не является частью объединения, то действуют следующие приоритеты:


  1. int;
  2. float;
  3. string;
  4. bool;

Так вот, будет перебираться этот список с типами, и для каждого проверяться: Если тип существует в объединении, и значение может быть приведёно к нему в соответствии с семантикой PHP, то так и будет сделано. Иначе пробуем следующий тип.


Как исключение, если string должен быть приведён к int|float, то сравнение идёт в первую очередь в соответствии с семантикой "числовых строк". К примеру, "123" станет int(123), в то время как "123.0" станет float(123.0).


К типам null и false не происходит неявного преобразования.

Таблица неявного приведения типов:


Original type 1st try 2nd try 3rd try
bool int float string
int float string bool
float int string bool
string int/float bool
object string

Типы полей и ссылки


class Test {    public int|string $x;    public float|string $y;}$test = new Test;$r = "foobar";$test->x =& $r;$test->y =& $r;// Reference set: { $r, $test->x, $test->y }// Types: { mixed, int|string, float|string }$r = 42; // TypeError

Здесь проблема в том, что тип устанавливаемого значения не совместим с объявленными в полях класса. Для Test::$x это могло быть int(42), а для Test::$y float(42.0). Так как эти значения не эквивалентны, то невозможно обеспечить единую ссылку, и TypeError будет сгенерирован.


24. Тип mixed (Mixed Type v2)


Был добавлен новый тип mixed.
Он эквивалентен типу array|bool|callable|int|float|object|resource|string|null.
Когда параметр объявлен без типа, то его тип это mixed.
Но стоит отметить, что если функция не объявляет возвращаемого значения, то это не mixed, а mixed|void. Таким образом, если функция гарантировано должна что-то возвращать, но тип результата не известен заранее, то стоит написать его mixed.


При наследовании действуют следующие правила:


class A{    public function bar(): mixed {}}class B extends A{    // return type was narrowed from mixed to int, this is allowed    public function bar(): int {}}

class C{    public function bar(): int {}}class D extends C{    // return type cannot be widened from int to mixed    // Fatal error thrown    public function bar(): mixed {}}

Подробнее можно почитать здесь


Где смотреть новые фичи


Более информации про новые функции в PHP можно посмотреть на rfc watch.


IMHO хорошие идеи для PHP



С текущей реализацией PHP можно писать программы практически не используя переназначение переменной. Мы могли бы для каждого нового значения создавать свою собственную переменную. Тем самым код стал бы куда лучше понятным, так как для каждой переменной нужно дать имя в соответствии с тем, что там храниться. Но сейчас так никто не делает. Отчасти это потому что язык не позволяет ограничить переприсваивание значения.


Вот пример проблемного кода. Со временем значение изменяется, и при добавлении ещё какой-либо обработки велика вероятность сломать что-то на следующих строках:


$invoice = getInvoice();$invoice = loadDependencies($invoice);$invoice = formatInvoice($invoice);// hm... how do I access initial $invoice now?return $invoice;

Я вижу как минимум 4 недостатка в этом коде:


  1. Никогда точно не знаешь что в переменной;
  2. Невозможность использовать уже перезаписанное значение где-то дальше в коде;
  3. Неустойчивость к изменениям если производиться копипаст большой части кода с такими-же переменными где-то во вложенном if, тогда ночь отладки обеспеченна.
  4. Каждый раз нужно писать знак $ перед $переменной. Да, это спорно, но ведь без долларов проще читать код. Возьмите какого-либо джависта, что он скажет про ваш код? Уххх как много долларов!

Вот каким мог быть этот код:


invoice = getInvoice();invoiceWithDependencies = loadDependencies(invoice);invoiceFormatted = formatInvoice(invoiceWithDependencies);// someAnotherAction(invoice);return invoiceFormatted;

Значения, что содержатся в invoice, invoiceWithDependencies, invoiceFormatted не могут быть перезаписаны. Да, и теперь мы точно знаем что и где хранится.


function printArr(array arr) {    foreach (arr as firmValue) {        print strtr(            "Current value is {current}. +1: {next}",             [                '{current}' => firmValue,                 '{next}'    => firmValue + 1            ]        );    }}


use Spatie\ModelStates\State;abstract class OrderStatus extends State{    public static string $name = static::getName();    abstract protected function getName(): string;}

Как видим, при первом обращении к $name, будет вызван метод getName финального класса. Это дает нам возможность настраивать какие значения будут попадать в поля в зависимости от каких-либо условий. А в данном примере это использовано с шаблоном "Template Method", и финальные классы обязаны предоставить нам значение для поля.


Сейчас многие фреймворки имеют значени по-умолчанию для большинства конфигураций в своих классах. Проблема с таким подходом заключается в том, что мы можем предоставить только примитивное значение. Никаких вызовов функций не разрешено. А что если мы хотим вызвать хелпер config для предоставления конфигурации, которая задаётся в поле класса? Тогда у нас проблемы, и нужно переопределять конструктор, где уже задавать значение поля. Хорошо, когда конструктор не имеет много параметров. А что, если там много параметров (к примеру, 7)? Тогда для простого создания поля, нужно 20 дополнительных бесполезных строк кода. И выглядит это ещё как уродливо!


Просто сравните это:


    protected string $whatever = $this->doCalculate();

И это:


    public function __construct(        array $query = [],        array $request = [],        array $attributes = [],        array $cookies = [],        array $files = [],        array $server = [],        $content = null    ) {        parent::__construct(            $query,            $request,            $attributes,            $cookies,            $files,            $server,            $content        );        $this->whatever = $this->doCalculate();    }

Почему мы должны инициализировать поле в конструкторе, если оно не зависит от его параметров? Как по мне, мы не должны.

Подробнее..

Java HotSpot JIT компилятор устройство, мониторинг и настройка (часть 1)

07.01.2021 02:21:44 | Автор: admin
JIT (Just-in-Time) компилятор оказывает огромное влияние на быстродействие приложения. Понимание принципов его работы, способов мониторинга и настройки является важным для каждого Java-программиста. В цикле статей из двух частей мы рассмотрим устройство JIT компилятора в HotSpot JVM, способы мониторинга его работы, а также возможности его настройки. В этой, первой части мы рассмотрим устройство JIT компилятора и способы мониторинга его работы.

AOT и JIT компиляторы


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

Существуют компилируемые языки программирования, такие как C и C++. Программы, написанные на этих языках, распространяются в виде машинного кода. После того, как программа написана, специальный процесс Ahead-of-Time (AOT) компилятор, обычно называемый просто компилятором, транслирует исходный код в машинный. Машинный код предназначен для выполнения на определенной модели процессора. Процессоры с общей архитектурой могут выполнять один и тот же код. Более поздние модели процессора как правило поддерживают инструкции предыдущих моделей, но не наоборот. Например, машинный код, использующий AVX инструкции процессоров Intel Sandy Bridge не может выполняться на более старых процессорах Intel. Существуют различные способы решения этой проблемы, например, вынесение критичных частей программы в библиотеку, имеющую версии под основные модели процессора. Но часто программы просто компилируются для относительно старых моделей процессоров и не используют преимущества новых наборов инструкций.

В противоположность компилируемым языкам программирования существуют интерпретируемые языки, такие как Perl и PHP. Один и тот же исходный код при таком подходе может быть запущен на любой платформе, для которой существует интерпретатор. Минусом этого подхода является то, что интерпретируемый код работает медленнее, чем машинный код, делающий тоже самое.

Язык Java предлагает другой подход, нечто среднее между компилируемыми и интерпретируемыми языками. Приложения на языке Java компилируются в промежуточный низкоуровневый код байт-код (bytecode).
Название байт-код было выбрано потому, что для кодирования каждой операции используется ровно один байт. В Java 10 существует около 200 операций.

Байт-код затем исполняется JVM также как и программа на интерпретируемом языке. Но поскольку байт-код имеет строго определенный формат, JVM может компилировать его в машинный код прямо во время выполнения. Естественно, старые версии JVM не смогут сгенерировать машинный код, использующий новые наборы инструкций процессоров вышедших после них. С другой стороны, для того, чтобы ускорить Java-программу, ее даже не надо перекомпилировать. Достаточно запустить ее на более новой JVM.

HotSpot JIT компилятор


В различных реализациях JVM JIT компилятор может быть реализован по-разному. В данной статье мы рассматриваем Oracle HotSpot JVM и ее реализацию JIT компилятора. Название HotSpot происходит от подхода, используемого в JVM для компиляции байт-кода. Обычно в приложении только небольшие части кода выполняются достаточно часто и производительность приложения в основном зависит от скорости выполнения именно этих частей. Эти части кода называются горячими точками (hot spots), их и компилирует JIT компилятор. В основе этого подхода лежит несколько суждений. Если код будет исполнен всего один раз, то компиляция этого кода пустая трата времени. Другая причина это оптимизации. Чем больше раз JVM исполняет какой либо код, тем больше статистики она накапливает, используя которую можно сгенерировать более оптимизированный код. К тому же компилятор разделяет ресурсы виртуальной машины с самим приложением, поэтому ресурсы затраченные на профилирование и оптимизацию могли бы быть использованы для исполнения самого приложения, что заставляет соблюдать определенный баланс. Единицей работы для HotSpot компилятора является метод и цикл.
Единица скомпилированного кода называется nmethod (сокращение от native method).

Многоуровневая компиляция (tiered compilation)


На самом деле в HotSpot JVM существует не один, а два компилятора: C1 и C2. Другие их названия клиентский (client) и серверный (server). Исторически C1 использовался в GUI приложениях, а C2 в серверных. Отличаются компиляторы тем, как быстро они начинают компилировать код. C1 начинает компилировать код быстрее, в то время как C2 может генерировать более оптимизированный код.

В ранних версиях JVM приходилось выбирать компилятор, используя флаги -client для клиентского и -server или -d64 для серверного. В JDK 6 был внедрен режим многоуровневой компиляции. Грубо говоря, его суть заключается в последовательном переходе от интерпретируемого кода к коду, сгенерированному компилятором C1, а затем C2. В JDK 8 флаги -client, -server и -d64 игнорируются, а в JDK 11 флаг -d64 был удален и приводит к ошибке. Выключить режим многоуровневой компиляции можно флагом -XX:-TieredCompilation.

Существует 5 уровней компиляции:
  • 0 интерпретируемый код
  • 1 C1 с полной оптимизацией (без профилирования)
  • 2 C1 с учетом количества вызовов методов и итераций циклов
  • 3 С1 с профилированием
  • 4 С2

Типичные последовательности переходов между уровнями приведены в таблице.
Последовательность
Описание
0-3-4 Интерпретатор, уровень 3, уровень 4. Наиболее частый случай.
0-2-3-4 Случай, когда очередь уровня 4 (C2) переполнена. Код быстро компилируется на уровне 2. Как только профилирование этого кода завершится, он будет скомпилирован на уровне 3 и, наконец, на уровне 4.
0-2-4 Случай, когда очередь уровня 3 переполнена. Код может быть готов к компилированию на уровне 4 все еще ожидая своей очереди на уровне 3. Тогда он быстро компилируется на уровне 2 и затем на уровне 4.
0-3-1 Случай простых методов. Код сначала компилируется на уровне 3, где становится понятно, что метод очень простой и уровень 4 не сможет скомпилировать его оптимальней. Код компилируется на уровне 1.
0-4 Многоуровневая компиляция выключена.

Code cache


Машинный код, скомпилированный JIT компилятором, хранится в области памяти называемой code cache. В ней также хранится машинный код самой виртуальной машины, например, код интерпретатора. Размер этой области памяти ограничен, и когда она заполняется, компиляция прекращается. В этом случае часть горячих методов так и продолжит выполняться интерпретатором. В случае переполнения JVM выводит следующее сообщение:

Java HotSpot(TM) 64-Bit Server VM warning: CodeCache is full.
Compiler has been disabled.

Другой способ узнать о переполнении этой области памяти включить логирование работы компилятора (как это сделать обсуждается ниже).
Code cache настраивается также как и другие области памяти в JVM. Первоначальный размер задаётся параметром -XX:InitialCodeCacheSize. Максимальный размер задается параметром -XX:ReservedCodeCacheSize. По умолчанию начальный размер равен 2496 KB. Максимальный размер равен 48 MB при выключенной многоуровневой компиляции и 240 MB при включенной.

Начиная с Java 9 code cache разделен на 3 сегмента (суммарный размер по-прежнему ограничен пределами, описанными выше):
  • JVM internal (non-method code). Содержит машинный код, относящийся к самой JVM, например, код интерпретатора. Размер этого сегмента зависит от количества потоков компиляции. На машине с четырьмя ядрами по умолчанию его размер составляет около 5.5 MB. Задать произвольный размер сегмента можно параметром -XX:NonNMethodCodeHeapSize.
  • Profiled code. Содержит частично оптимизированный машинный код с коротким временем жизни. Размер этого сегмента равен половине пространства оставшегося после выделения non-method code сегмента. По умолчанию это 21.2 MB при выключенной многоуровневой компиляции и 117.2 MB при включенной. Задать произвольный размер можно параметром -XX:ProfiledCodeHeapSize.
  • Non-profiled code. Содержит полностью оптимизированный код с потенциально долгим временем жизни. Размер этого сегмента равен половине пространства оставшегося после выделения non-method code сегмента. По умолчанию это 21.2 MB при выключенной многоуровневой компиляции и 117.2 MB при включенной. Задать произвольный размер можно параметром -XX: NonProfiledCodeHeapSize.

Мониторинг работы компилятора


Включить логирование процесса компиляции можно флагом -XX:+PrintCompilation (по умолчанию он выключен). При установке этого флага JVM будет выводить в стандартный поток вывода (STDOUT) сообщение каждый раз после компиляции метода или цикла. Большинство сообщений имеют следующий формат: timestamp compilation_id attributes tiered_level method_name size deopt.
Поле timestamp это время со старта JVM.
Поле compilation_id это внутренний ID задачи. Обычно он последовательно увеличивается в каждом сообщении, но иногда порядок может нарушаться. Это может произойти в случае, если существует несколько потоков компиляции работающих параллельно.
Поле attributes это набор из пяти символов, несущих дополнительную информацию о скомпилированном коде. Если какой-то из атрибутов не применим, вместо него выводится пробел. Существуют следующие атрибуты:
  • % OSR (on-stack replacement);
  • s метод является синхронизированным (synchronized);
  • ! метод содержит обработчик исключений;
  • b компиляция произошла в блокирующем режиме;
  • n скомпилированный метод является оберткой нативного метода.

Аббревиатура OSR означает on-stack replacement. Компиляция это асинхронный процесс. Когда JVM решает, что метод необходимо скомпилировать, он помещается в очередь. Пока метод компилируется, JVM продолжает исполнять его интерпретатором. В следущий раз, когда метод будет вызван снова, будет выполняться его скомпилированная версия. В случае долгого цикла ждать завершения метода нецелесообразно он может вообще не завершиться. JVM компилирует тело цикла и должна начать исполнять его скомпилированную версию. JVM хранит состояние потоков в стеке. Для каждого вызываемого метода в стеке создается новый объект Stack Frame, который хранит параметры метода, локальные переменные, возвращаемое значение и другие значения. Во время OSR создается новый объект Stack Frame, который заменяет собой предыдущий.

Источник: The Java HotSpotTM Virtual Machine Client Compiler: Technology and Application
Атрибуты s и ! в пояснении я думаю не нуждаются.
Атрибут b означает, что компиляция произошла не в фоне, и не должен встречаться в современных версиях JVM.
Атрибут n означает, что скомпилированный метод является оберткой нативного метода.
Поле tiered_level содержит номер уровня, на котором был скомпилирован код или может быть пустым, если многоуровневая компиляция выключена.
Поле method_name содержит название скомпилированного метода или название метода, содержащего скомпилированный цикл.
Поле size содержит размер скомпилированного байт-кода, не размер полученного машинного кода. Размер указан в байтах.
Поле deopt появляется не в каждом сообщении, оно содержит название проведенной деоптимизации и может содержать такие сообщения как made not entrant и made zombie.
Иногда в логе могут появиться записи вида: timestamp compile_id COMPILE SKIPPED: reason. Они означают, что при компиляции метода что-то пошло не так. Есть случаи, когда это ожидаемо:
  • Code cache filled необходимо увеличть размер области памяти code cache.
  • Concurrent classloading класс был модифицирован во время компиляции.

Во всех случаях, кроме переполнения code cache, JVM попробует повторить компиляцию. Если этого не происходит, можно попробовать упростить код.

В случае, если процесс был запущен без флага -XX:+PrintCompilation, взглянуть на процесс компиляции можно с помощью утилиты jstat. У jstat есть два параметра для вывода информации о компиляции.
Параметр -compiler выводит сводную информацию о работе компилятора (5003 это ID процесса):
% jstat -compiler 5003
Compiled Failed Invalid Time FailedType FailedMethod
206 0 0 1.97 0

Эта команда также выводит количество методов, компиляция которых завершилась ошибкой и название последнего такого метода.
Параметр -printcompilation выводит информацию о последнем скомпилированном методе. В сочетании со вторым параметром периодом повторения операции, можно наблюдать процесс компиляции с течением времени. В следующем примере команда -printcompilation выполняется каждую секунду (1000 мс):
% jstat -printcompilation 5003 1000
Compiled Size Type Method
207 64 1 java/lang/CharacterDataLatin1 toUpperCase
208 5 1 java/math/BigDecimal$StringBuilderHelper getCharArray

Планы на вторую часть


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

Список литературы и ссылки


Подробнее..

Перевод Язык программирования PHP 8 новый JIT-компилятор нацелен на лучшую производительность

08.01.2021 02:22:10 | Автор: admin

Привет, Хабр. Будущих студентов курса "PHP-разработчик" и всех интересующихся приглашаем принять участие в открытом вебинаре на тему "PHP 8 Что нового?".

А сейчас делимся традиционным переводом интересного материала.


Старший эксперт по базам данных Google в восторге от JIT-компилятора, но остальные сомневаются в его значимости и говорят, что его сложно поддерживать.

Команда, создавшая скриптовый язык PHP, анонсировала версию PHP 8.0 крупный релиз, который может потребовать от разработчиков проверку кода на предмет критических изменений.

Эта версия 25-летнего PHP представляет улучшенную систему типов, новый JIT-компилятор в движке PHP и некоторые функции, заимствованные из Python и JavaScript, такие как именованные аргументы и null safe операторы.

PHP как язык не обрел всеобщей любви, но он широко используется веб-разработчиками. Разработчики оценивают PHP как шестой самый страшный язык в опросе разработчиков Stack Overflow 2020 года, но он также оказался восьмым по популярности языком.

Аналитик-разработчик RedMonk в настоящее время позиционирует PHP как четвертый по популярности язык, уступающий только Java, Python и JavaScript. Tiobe Software ставит PHP на восьмое место. И, по данным поисковой системы Indeed, количество объявлений о вакансиях PHP-разработчиков начального уровня за последний год увеличилось более чем на 800%.

PHP поддерживается основной группой разработчиков PHP и Zend, американской консалтинговой компанией по разработке PHP, основанной Энди Гутмансом, генеральным менеджером и вице-президентом по разработке баз данных в Google. Гутманс сказал, что он "в восторге" от JIT-компилятора.

JIT-компилятор предназначен для улучшения производительности веб-приложений. Однако Брент Руз, бельгийский разработчик stitcher.io, сказал, что у него есть нарекания, когда дело касается запросов.

Как у интерпретируемого языка, такого как JavaScript и Python, код PHP транслируется во время выполнения. Это не компилируемый язык, такой как C, Java или Rust, и его нужно транслировать, чтобы ЦП понимал код PHP.

JIT-компилятор может значительно улучшить производительность вашей программы, но сделать это правильно сложно, отметил Руз.

Плюсы JIT-компилятора заключаются в том, что он может сделать PHP лучшим языком для использования вне сети, но может затруднить отладку кода в компиляторе. Руз отметил, что в случае ошибки пользователи PHP будут зависеть от мейнтейнеров JIT-компилятора, что может означать более медленные исправления по части безопасности и багов.

Поскольку сегодня лишь несколько человек могут поддерживать кодовую базу [PHP], вопрос о том, возможно ли поддерживать JIT-компилятор как подобает, кажется оправданным. Конечно, люди могут разобраться в работе компилятора. Но, как ни крути, это сложный материал, сказал Руз.

Он сомневается, следует ли включать компилятор в PHP из-за затрат на обслуживание. Он также сказал, что пользователи PHP должны знать, что некоторые исправления ошибок или обновления версий могут занять больше времени, чем то, что мы привыкли ожидать на сегодняшний день.

Кроме того, выпуск PHP 8 в качестве мажорного релиза может означать, что старый код PHP может стать нерабочим после обновления. Однако Руз отметил, что большинство критических изменений объявлены устаревшими до версий 7.x.

Zend также опубликовал напоминание о том, что жизненный цикл PHP 7.2 подходит к концу 30 ноября 2020 года, а это означает, что основные контрибьюторы PHP больше не будут предлагать патчи безопасности для этой версии, что может создать проблемы для корпоративных веб-приложений.

Но пользователи PHP всегда могут заплатить консалтинговой фирме, такой как Zend, за долгосрочную поддержку, и они получат патчи после 30 ноября.

Что до пользователей операционных систем с предложениями долгосрочной поддержки, то их бинарники PHP зачастую продолжают получать патчи от поставщика ОС, даже когда заканчивается период поддержки сообщества", сказал Мэтью Вейер О'Финни, инженер Zend.

В случае с Ubuntu 18.04 и RHEL/CentOS 8, каждая из которых поставляется с PHP 7.2, это означает, что вы можете продолжать получать патчи. Однако если операционная система, в которой вы работаете, не находится под политикой LTS, тогда ваша версия со временем станет уязвимым для новых эксплойтов".

Марк Стори, главный разработчик Sentry, который поддерживает проекты PHP CakePHP и XHGui, сказал, что именованные параметры и типы объединения PHP 8 обеспечивают улучшения эргономичности и корректности.

Именованные параметры позволяют вызывать методы с параметрами на основе их имени, а не только их порядка, объяснил Стори.

Именованные параметры помогают упростить использование методов вызова, которые имеют множество необязательных параметров, поскольку вы можете указать только те параметры, которые используете по имени, сказал он ZDNet.

Это также поможет улучшить читаемость кода в будущем, так как запомнить, какой из параметров метода шестой труднее, чем понять, что может делать параметр expires.

Между тем, типы объединений расширяют систему типов PHP.


Узнать подробнее о курсе "PHP-разработчик".

Зарегистрироваться на открытый вебинар на тему "PHP 8 Что нового?".

Прямо сейчас в OTUS действуют максимальные новогодние скидки на все курсы. Ознакомиться с полным списком курсов вы можете по ссылке ниже. Также у всех желающих есть уникальная возможность отправить адресатуподарочный сертификат на обучение в OTUS.

Кстати, о "красивой упаковке" онлайн-сертификатов мырассказываем в этой статье.

ЗАБРАТЬ СКИДКУ

Подробнее..

Java HotSpot JIT компилятор устройство, мониторинг и настройка (часть 2)

11.01.2021 02:05:19 | Автор: admin

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

Счетчики вызовов методов и итераций циклов

Главным фактором влияющим на решение JVM о компиляции какого-либо кода является частота его исполнения. Решение принимается на основе двух счетчиков: счетчика количества вызовов метода и счетчика количества итераций циклов в методе.

Когда JVM исполняет какой-либо метод, она проверяет значения этих двух счетчиков, и принимает решение о необходимости его компиляции. У этого типа компиляции нет официального названия, но часто его называют стандартной компиляцией.

Аналогично, после каждой итерации цикла проверяется значение счетчика цикла и принимается решение о необходимости его компиляции.

При выключенной многоуровневой компиляции стандартная компиляция управляется параметром -XX:CompileThreshold. Значением по умолчанию является 10000. Несмотря на то, что параметр всего один, превышение порога определяется суммой значений двух счетчиков. В настоящее время этот флаг ни на что не влияет (многоуровневая компиляция включена по умолчанию), но знать о нем полезно, ведь существует довольно много унаследованных систем.

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

Последнее утверждение довольно интересно. Ведь если программа исполняется бесконечно, не должен ли весь ее код в конце концов скомпилироваться? На самом деле не должен, поскольку значения счетчиков не только увеличиваются при каждом вызове метода или итерации цикла, но и периодически уменьшаются. Таким образом, они являются отражением текущего нагрева метода или цикла.

Получается, что до внедрения многоуровневой компиляции методы, выполнявшиеся довольно часто, но недостаточно часто, чтобы превысить порог, никогда бы не были скомпилированы. В настоящее время такие методы будут скомпилированы компилятором C1, хотя, возможно, их производительность была бы выше, будь они скомпилированы компилятором C2. При желании можно поиграть параметрами -XX:Tier3InvocationThreshold (значение по умолчанию 200) и -XX:Tier4InvocationThreshold (значение по умолчанию 5000), но вряд ли в этом есть большой практический смысл. Такие же параметры (-XX:TierXBackEdgeThreshold) существуют и для задания пороговых значений счетчиков циклов.

Потоки компиляции

Как только JVM решает скомпилировать метод или цикл, они помещаются в очередь. Эта очередь приоритетная - чем выше значения счетчиков, тем выше приоритет. Это особенно помогает при старте приложения, когда необходимо компилировать огромное количество кода. Таким образом, более важный код будет скомпилирован раньше.

Компиляторы C1 и C2 имеют собственные очереди, каждая из которых может обрабатывается несколькими потоками. Существует специальная формула для вычисления количества потоков в зависимости от количества ядер. Некоторые значения приведены в таблице:

Количество ядер

Количество потоков C1

Количество потоков C2

1

1

1

2

1

1

4

1

2

8

1

2

16

2

6

32

3

7

64

4

8

128

4

10

Задать произвольное количество потоков можно параметром -XX:CICompilerCount. Это общее количество потоков компиляции, которое будет использовать JVM. При включенной многоуровневой компиляции одна треть из них (но минимум один) будут отданы компилятору C1, остальные достанутся компилятору C2. Значением по умолчанию для этого флага является сумма потоков из таблицы выше. При выключенной многоуровневой компиляции все потоки достанутся компилятору C2.

В каком случае имеет смысл менять настройки по умолчанию? Ранние версии Java 8 (до update 191) при запуске в Docker контейнере не корректно определяли количество ядер. Вместо количества ядер, выделенных контейнеру определялось количество ядер на сервере. В этом случае есть смысл задать количество потоков вручную, исходя из значений, приведенных в таблице выше.

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

Еще один параметр, влияющий на количество потоков компиляции - это -XX:+BackgroundCompilation. Его значение по умолчанию - true. Он означает, что компиляция должна происходить в асинхронном режиме. Если установить его в false, каждый раз при наличии кода, который необходимо скомпилировать, JVM будет ожидать завершения компиляции, прежде чем этот код исполнить.

Оптимизации

Как мы знаем, JVM использует результаты профилирования методов в процессе компиляции. JVM хранит данные профиля в объектах, называемых method data objects (MDO). Объекты MDO используются интерпретатором и компилятором C1 для записи информации, которая затем используется для принятия решения о том, какие оптимизации возможно применить к компилируемому коду. Объекты MDO хранят информацию о вызванных методах, выбранных ветвях в операторах ветвления, наблюдаемых типах в точках вызова. Как только принято решение о компиляции, компилятор строит внутреннее представление компилируемого кода, которое затем оптимизируется. Компиляторы способны проводить широкий набор оптимизаций, включающий:

  • встраивание (inlining);

  • escape-анализ (escape-analysis);

  • размотка (раскрутка) цикла (loop unrolling);

  • мономорфная диспетчеризация (monomorphic dispatch);

  • intrinsic-методы.

Встраивание

Встраивание - это копирование вызываемого метода в место его вызова. Это позволяет устранить накладные расходы, связанные с вызовом метода, такие как:

  • подготовка параметров;

  • поиск по таблице виртуальных методов;

  • создание и инициализация объекта Stack Frame;

  • передача управления;

  • опциональный возврат значения.

Встраивание является одной из оптимизаций, выполняемых JVM в первую очередь, оно включено по умолчанию. Отключить его можно флагом -XX:-Inline, хотя делать этого не рекомендуется. JVM принимает решение о необходимости встраивания метода на основе нескольких факторов, некоторые из которых приведены ниже.

  • Размер метода. Горячие методы являются кандидатами для встраивания если их размер меньше 325 байт (или меньше размера заданного параметром -XX:MaxFreqInlineSize). Если метод вызывается не так часто, он является кандидатом для встраивания только если его размер меньше 35 байт (или меньше размера заданного параметром -XX:MaxInlineSize).

  • Позиция метода в цепочке вызовов. Не подлежат встраиванию методы с позицией больше 9 (или значения заданного параметром -XX:MaxInlineLevel).

  • Размер памяти, занимаемой уже скомпилированными версиями метода в code cache. Встраиванию не подлежат методы, скомпилированные на последнем уровне, версии которых занимают более 1000 байт при выключенной многоуровневой компиляции и 2000 байт при включенной (или значения заданного параметром -XX:InlineSmallCode).

Escape-анализ

Escape-анализ - это техника анализа кода, которая позволяет определить пределы достижимости какого-либо объекта. Например, escape-анализ может использоваться для определения является ли объект созданный внутри метода достижимым за пределами области видимости метода. Сам по себе escape-анализ не является оптимизацией, но оптимизации могут выполняются по его результатам.

Предотвращение выделений памяти в куче

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

Блокировки и escape-анализ

JVM способна использовать результаты escape-анализа для оптимизации производительности блокировок. Это относится только к блокировкам с помощью ключевого слова synchronized, блокировки из пакета java.util.concurrent таким оптимизациям не подвержены. Возможными оптимизации приведены ниже.

  • Удаление блокировок с объектов недостижимых за пределами области видимости (lock elision).

  • Объединение последовательных синхронизированных секций, использующих один и тот же объект синхронизации (lock coarsening). Выключить эту оптимизацию можно флагом -XX:-EliminateLocks.

  • Определение и удаление вложенных блокировок на одном и том же объекте (nested locks). Выключить эту оптимизацию можно флагом -XX:-EliminateNestedLocks.

Ограничения escape-анализа

Поскольку регистры процессора и стек являются ресурсами довольно ограниченными, существуют ограничения и на их использование. Так, например, массивы размером более 64 элементов не участвуют в escape-анализе. Этот размер можно задавать параметром -XX:EliminateAllocationArraySizeLimit. Представьте код, создающий временный массив в цикле. Если массив не достижим за пределами метода, массив не должен создаваться в куче. Но если его размер больше 64 элементов, он будет создаваться именно там, даже при условии, что реально используется не весь массив.

Еще одно ограничение заключается в том, что частичный escape-анализ не поддерживается. Если объект выходит за пределы области видимости метода хотя бы по одной из веток, оптимизация по предотвращению создания объекта в куче не применима. Пример подобного кода приведен ниже.

for (int i = 0; i < 100_000_000; i++) {    Object mightEscape = new Object(i);    if (condition) {        result += inlineableMethod(mightEscape);    } else {        result += tooBigToInline(mightEscape);    }}

Но если вам удастся локализовать создание объекта внутри ветки, в которой объект не выходит за пределы области видимости, то данная оптимизация будет применена в этой ветке.

if (condition) {        Object mightEscape = new Object(i);        result += inlineableMethod(mightEscape);    } else {        Object mightEscape = new Object(i);        result += tooBigToInline(mightEscape);    }}

Размотка (раскрутка) цикла

После встраивания всех возможных вызовов методов внутри цикла, JVM может оценить стоимость каждой его итерации, и определить возможность применения оптимизации, называемой размотка (раскрутка) цикла. Размотка цикла - это техника оптимизации компьютерных программ, состоящая в искусственном увеличении количества инструкций, исполняемых в течение одной итерации цикла. Каждая итерация цикла оказывает отрицательное влияние на работу процессора, т.к. сбрасывает конвейер инструкций. Чем короче тело цикла, тем выше стоимость итерации.

В результате размотки цикла такой код:

int i;for ( i = 1; i < n; i++){    a[i] = (i % b[i]);}

преобразуется в код вида:

int i;for (i = 1; i < n - 3; i += 4){    a[i] = (i % b[i]);    a[i + 1] = ((i + 1) % b[i + 1]);    a[i + 2] = ((i + 2) % b[i + 2]);    a[i + 3] = ((i + 3) % b[i + 3]);}for (; i < n; i++) {    a[i] = (i % b[i]);}

JVM принимает решение о размотке цикла по нескольким критериям:

  • по типу счетчика цикла, он должен быть одним из типов int, short или char;

  • по значению, на которое меняется счетчик цикла каждую итерацию;

  • по количеству точек выхода из цикла.

Мономорфная диспетчеризация

Многие оптимизации, выполняемые компилятором C2 основаны на эмпирических наблюдениях. Одним из примеров является оптимизация под названием мономорфная диспетчеризация. Она основана на факте, что очень часто в точках вызова тип объекта во время выполнения остается неизменным. Это связано с особенностями объектно-ориентированного дизайна. Например, при вызове метода на объекте, наблюдаемый тип объекта при первом и последующих вызовах будет одним и тем же. Если это предположение верно, то вызов метода в этой точке можно оптимизировать. В частности, нет необходимости каждый раз выполнять поиск по таблице виртуальных методов. Достаточно один раз определить целевой тип объекта и заменить вызов виртуального метода быстрой проверкой типа и прямым вызовом метода на объекте целевого типа. Если в какой-то момент тип объекта поменяется, JVM откатит оптимизацию и будет снова выполнять вызов виртуального метода.

Большое число вызовов в типичном приложении являются мономорфными. JVM также поддерживает биморфную диспетчеризацию. Она позволяет делать быстрые вызовы методов в одной точке на объектах двух разных типов.

Вызовы, которые не являются ни мономорфными ни биморфными, называются мегаморфными. Если в точке вызова наблюдается не очень большое число типов, используя один трюк, можно немного выиграть в производительности. Достаточно отделить от точки вызова несколько типов, используя оператор instanceof так, чтобы в ней осталось только 2 конкретных типа. Примеры биморфного, мегаморфного и разделенного мегаморфного вызовов приведены ниже.

interface Shape {int getSides();}class Triangle implements Shape {public int getSides() {return 3;}}class Square implements Shape {public int getSides() {return 4;}}class Octagon implements Shape {public int getSides() {return 8;}}class Example {  private Random random = new Random();private Shape triangle = new Triangle();private Shape square = new Square();private Shape octagon = new Octagon();public int getSidesBimorphic() {Shape currentShape = null;switch (random.nextInt(2)) {case 0:currentShape = triangle;break;case 1:currentShape = square;break;}return currentShape.getSides();}  public int getSidesMegamorphic() {    Shape currentShape = null;    switch (random.nextInt(3))    {    case 0:      currentShape = triangle;      break;    case 1:      currentShape = square;      break;    case 2:      currentShape = octagon;      break;    }    return currentShape.getSides();}  public int getSidesPeeledMegamorphic() {    Shape currentShape = null;    switch (random.nextInt(3))    {    case 0:      currentShape = triangle;      break;    case 1:      currentShape = square;      break;    case 2:      currentShape = octagon;      break;    }    // peel one observed type from the original call site    if (currentShape instanceof Triangle) {      return ((Triangle) currentShape).getSides();    }    else {      return currentShape.getSides(); // now only bimorphic    }}}

Intrinsic-методы

Intrinsic-методы - это оптимизированные нативные реализации методов готовые к использованию JVM. Обычно это базовые, критичные к производительности методы, использующие специфичные функции операционной системы (ОС) или архитектуры процессора. Из-за этого они являются платформо-зависимыми и некоторые из них могут поддерживаться не каждой платформой. Примеры intrinsic-методов приведены в таблице ниже.

Метод

Описание

java.lang.System.arraycopy()

Быстрое копирование, используя векторную поддержку процессора.

java.lang.System.currentTimeMillis()

Быстрая реализация предоставляемая большинством ОС.

java.lang.Math.min()

Может быть выполнено без ветвления на некоторых процессорах.

Другие методы класса java.lang.Math

Прямая поддержка инструкций некоторыми процессорами.

Криптографические функции

Может использоваться аппаратная поддержка на некоторых платформах.

Шаблоны intrinsic-методов содержатся в исходном коде OpenJDK в файлах с расширением .ad (architecture dependent). Для архитектуры x86_64 они находятся в файле hotspot/src/cpu/x86/vm/x86_64.ad.

Деоптимизации

Когда мы рассматривали мониторинг работы компилятора в первой части, мы упомянули, что в логе могут появиться сообщения о деоптимизации кода. Деоптимизация означает откат ранее скомпилированного кода. В результате деоптимизации производительность приложения будет временно снижена. Существует два вида деоптимизации: недействительный код (not entrant code) и зомби код (zombie code).

Недействительный код

Код может стать недействительным в двух случаях:

  • при использовании полиморфизма;

  • в случае многоуровневой компиляции.

Полиморфизм

Рассмотрим пример:

Validator validator;if (document.isOrder()) {  validator = new OrderValidator();} else {  validator = new CommonValidator();}ValidationResult validationResult = validator.validate(document);

Выбор валидатора зависит от типа документа. Пусть для заказов у нас есть собственный валидатор. Предположим, что необходимо провалидировать большое количество заказов. Компилятор зафиксирует, что всегда используется валидатор заказов. Он встроит метод validate (если это возможно) и применит другие оптимизации. Далее, если на валидацию придет документ другого типа, предыдущее предположение окажется неверным, и сгенерированный код будет помечен как недействительный (non entrant). JVM перейдет на интерпретацию этого кода, и в будущем сгенерирует новую его версию.

Многоуровневая компиляция

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

Зомби код

Если в логе компиляции появилось сообщение о зомби коде, это значит, что все объекты, использующие предыдущие оптимизации были удалены из памяти и, как следствие, из code cache был удален код, ранее помеченный недействительным.

Список литературы и ссылки

  • Java Performance: In-Depth Advice for Tuning and Programming Java 8, 11, and Beyond, Scott Oaks. ISBN: 978-1-492-05611-9.

  • Optimizing Java: Practical Techniques for Improving JVM Application Performance, Benjamin J. Evans, James Gough, and Chris Newland. ISBN: 978-1-492-02579-5.

  • Размотка цикла - википедия

Статьи цикла

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru