Ниже — разбор вопросов с моего прошедшего ранее собеседования в Avito.Tech, на должность веб-разработчика php/go. Как известно, Авито — крупная ИТ-компания, входящая в условный “российский FAANG” или топ Тех-компаний ИТ отрасли. Попасть сюда сложно, отбор кандидатов долгий и многоступенчатый. Поэтому успешно пройти все этапы интервью сюда — это большой успех и показатель хорошей технической подготовки кандидата. Что ж, ниже мы разберем вопросики, на которых “сыпятся“ большинство из кандидатов.

/**/

Вопросы


Есть ли опыт работы с Golang ?


Каково внутреннее устройство массивов в php ? Как растет память при передаче массива в ф-ю ?

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

Массивы в PHP также поддерживают автоматическое расширение при добавлении новых элементов и автоматическое сжатие при удалении элементов. Устройство в виде хеш-таблицы позволяет находить в них данные по ключу за единичное время О(1).

(звезда) Интересный факт (по ссылке ниже): С ростом размера массива, время вставки в него и объем потребления памяти увеличиваются нелинейно.

Дело в том, что на уровне C (да и вообще на системном уровне), не бывает массивов, с нефиксированным размером. Каждый раз, когда вы создаете массив в C, вы должны указать его размер, чтобы система знала, сколько нужно памяти на него выделить.

Тогда как это реализовано в PHP и как объяснить те скачки на графике?

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

Подробнее:

Как выделяется память под массив, передаваемый в ф-ю в качестве аргумента?

(warning) Массив передается в функцию изначально по ссылке (а не по значению, как принято думать); поэтому лишняя память не расходуется; но при любом его изменении создается уже копия этого массива, т.о. выделяется память под копию этого массива.

Это механизм copy-on-write («копирование при записи»), используемый в PHP для передачи массивов в функции.

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

  • Пока массив только читается — изменений нет и копия не создается.

  • Как только происходит запись в массив (изменение элемента) — создается полная копия массива и дальнейшие операции уже идут с этой копией.

  • Исходный массив, переданный в функцию, остается неизменным.

Таким образом оптимизируется производительность и память при передаче массивов в функции в PHP. Этот механизм автоматически применяется PHP при работе с массивами.


Что нового в последних версиях php?

Версия php

Что нового

Подробнее

8.0

PHP 8.0 был выпущен в ноябре 2020 года и включает ряд новых функций и улучшений. Некоторые из них включают:

Добавлены новые синтаксические конструкции:

  • Добавлена поддержка атрибутов, которые позволяют добавлять метаданные к классам, свойствам, методам и функциям.

  • Добавлена возможность использовать строгую типизацию возвращаемого значения для конструкторов и магических методов.

  • Добавлена конструкция «match«, которая предоставляет более удобный синтаксис для множественного сравнения значений.

Улучшения в существующих функциях:

  • Функция «str_contains()» была добавлена для проверки наличия подстроки в строке.

  • Функция «str_starts_with()» была добавлена для проверки, начинается ли строка с определенной подстроки.

  • Функция «str_ends_with()» была добавлена для проверки, заканчивается ли строка определенной подстрокой.

  • Функция «get_debug_type()» была добавлена для получения типа переменной в отладочных целях.

Улучшения производительности:

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

  • Введены Just-In-Time (JIT) компиляция, что ускоряет выполнение кода.

Другие изменения:

  • Добавлены новые расширения: FFI (Foreign Function Interface) было обновлено для добавления новых функций и возможностей.

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

Wiki

8.1

PHP 8.1 представляет несколько новых функций и улучшений, включая:

  • Enum-свойства классов (перечисления)

  • Свойство “только для чтения“: readonly — свойства классов

  • Файберы: лекговесные потоки исполнения (~coroutines in Golang)

  • Оператор fdiv(): Добавлен новый оператор fdiv(), который облегчает деление чисел с плавающей точкой без ошибок округления.

  • Изменения в синтаксисе: В PHP 8.1 добавлены некоторые изменения в синтаксис, такие как поддержка атрибутов и изменения в синтаксисе объявления параметров функции.

  • Поддержка типов: PHP 8.1 предлагает улучшенную поддержку типов, включая поддержку типа «never» и возможность указания типов для локальных переменных.

  • Улучшения в работе с массивами: PHP 8.1 предлагает новые функции для работы с массивами, такие как array_is_list(), array_contains(), array_remove(), array_union(), array_intersection() и другие.

  • Улучшения в работе с строками: Добавлены новые функции для работы со строками, такие как str_contains(), str_starts_with(), str_ends_with() и другие.

  • Улучшения в работе с классами: PHP 8.1 предлагает новые возможности для работы с классами, такие как возможность объявления свойств только для чтения и возможность указания типов для статических свойств.

  • Улучшения в работе с асинхронным программированием: PHP 8.1 предлагает улучшенную поддержку асинхронного программирования с помощью новых функций, таких как stream_await(), stream_async_enable(), stream_async_disable() и других.

Wiki

8.2

  • Readonly-классы

  • Типы в виде дизъюнктивной нормальной формы (ДНФ)

  • Самостоятельные типы null, false и true

  • Модуль «Random»

  • Можно объявлять константы внутри трейтов

  • Динамические свойства объявлены устаревшими

  • Добавлено #[SensitiveParameter]

  • Расширение enum в константах

  • Расширена рефлексия: можно узнать, анонимный ли метод —

    и есть ли у метода прототип

  • Новые функции memory_reset_peak_usage

Wiki

8.3

Wiki

Подробнее :


В чем разница между jit и opcache

Вот основные различия между jit и opcache в PHP:

  • JIT (Just-In-Time) компиляция — это технология, которая компилирует PHP код в машинный код во время выполнения скрипта. Это позволяет ускорить выполнение кода, так как машинный код выполняется быстрее, чем интерпретируемый bytecode. JIT был введен в PHP с версии 7.4.

  • OPcache — это кэш компилированного bytecode, который сохраняет предварительно скомпилированный bytecode скриптов PHP в памяти. Это позволяет избежать повторной компиляции скриптов при каждом запросе, что даёт прирост производительности. OPcache присутствует в PHP начиная с версии 5.5.

  • JIT работает во время выполнения кода и компилирует его в машинные инструкции «на лету», в то время как OPcache просто кэширует уже скомпилированный bytecode между запросами.

  • JIT может дополнительно оптимизировать выполнение кода за счёт информации о типах данных и других факторов во время выполнения. OPcache такой оптимизации не производит.

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


Что такое zend vm

Zend VM — это виртуальная машина (ВМ) для выполнения bytecode в PHP.

Основные функции Zend VM:

  • Интерпретация bytecode — Zend VM выполняет предварительно скомпилированный в bytecode код PHP, интерпретируя его.

  • Управление памятью — выделяет и освобождает память в процессе выполнения скриптов.

  • Обеспечение безопасности — проверяет доступ к ресурсам в соответствии с настройками безопасности.

  • Оптимизации — применяет различные оптимизации исполнения кода, например кэширование часто используемых функций.

  • Поддержка расширений — интегрирует расширения в ВМ, обеспечивая возможность вызова их функций.

Zend VM является ключевым компонентом PHP, обеспечивающим выполнение скриптов. Современные версии Zend VM используют JIT-компиляцию кода в машинные инструкции для ускорения выполнения.

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


В чем разница между генератором и итератором?

Основные различия между генераторами и итераторами в PHP:

  • Генераторы (yield) позволяют возвращать значения по одному при каждом вызове функции или метода. Итераторы реализуют интерфейс, который позволяет поочередно получать элементы из коллекции.

  • Генераторы более легковесны и производительны, т.к. возвращают значения «на лету» без создания коллекции. Итераторы предполагают наличие некой структуры данных, по которой происходит итерация.

  • С генераторами код получается более линейным и читаемым. Итераторы требуют реализации определенных методов интерфейса.

  • Итераторы можно перематывать, получая предыдущие значения. Генераторы движутся строго вперед.

  • Генератор может как возвращать значения через yield, так и получать входные данные через отправку значений в генератор. Итераторы только возвращают значения.

  • Итераторы хороши для обхода структур данных. Генераторы удобны для ленивых вычислений значений по запросу.

Таким образом, генераторы и итераторы решают схожие задачи, но генераторы обычно предпочтительнее для «ленивых» вычислений, а итераторы — для работы с данными. Их можно комбинировать в одном коде.


Как работает сборщик мусора в phpM

В PHP работает автоматический сборщик мусора на основе подсчета ссылок.

Основные принципы его работы:

  • Память выделяется для каждого объекта при создании.

  • Вместе с выделенной памятью сохраняется счетчик ссылок на объект.

  • При присваивании объекта переменной или передаче в функцию счетчик увеличивается.

  • При удалении переменной или возврата из функции счетчик уменьшается.

  • Когда счетчик достигает 0 — память объекта освобождается сборщиком мусора.

  • Сборка мусора запускается автоматически при освобождении памяти.

  • Также сборщик мусора может запускаться принудительно при помощи функции gc_collect_cycles().

  • Сборщик мусора ищет циклические ссылки между объектами и освобождает их.

Таким образом обеспечивается автоматическое управление памятью в PHP без необходимости ручного удаления объектов.


Что такое z_val ?

z_val в PHP — это внутреннее представление значения переменной в Zend Engine.

Когда в скрипте PHP объявляется переменная, для нее выделяется место в памяти. Это место в памяти содержит не само значение переменной, а указатель (zval) на это значение.

Структура zval содержит следующие поля:

  • zvalue — указатель на фактическое значение переменной

  • type — тип значения (string, int, double, array и т.д.)

  • is_ref — является ли переменная ссылкой

  • refcount — счетчик ссылок для сборщика мусора

Таким образом, z_val — это указатель на структуру zval, которая описывает переменную, ее тип и значение.

Использование zval позволяет реализовать в Zend Engine:

  • Проверку типов

  • Корректную работу ссылочных переменных

  • Подсчет ссылок для сборщика мусора

  • Изменение типа значения при необходимости

То есть z_val является базовой абстракцией, которая представляет значение переменной внутри PHP интерпретатора.


Как добиться единого code-style в команде разработчиков?

Кратко: линтеры, код-ревью, форматтеры кода

Вот несколько способов добиться единого code style в команде разработчиков:

  1. Выбрать готовое php code style руководство, например PSR стандарты. Описать стандарты и следовать им.

  2. Настроить линтер (phpcs, phpstan) с нужными правилами форматирования кода и запускать перед коммитом в репозиторий.

  3. Использовать форматтеры кода (phpcbf) для автоматического форматирования перед коммитом.

  4. Встроить проверку code style в CI/CD, чтобы код без соблюдения стандартов не попадал в основную ветку.

  5. Использовать Prettier или другие плагины форматирования в редакторах кода разработчиков.

  6. Проводить code review с проверкой соответствия стандартам.

  7. Организовать обучение команды по выбранным стандартам PSR

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


Как отловить ошибки в коде , которые не видит линтер?

В дополнение к линтерам, для поиска ошибок в коде, которые они не выявляют, можно использовать следующие способы:

  • Unit тесты — позволяют выявить логические ошибки в коде.

  • Интеграционное тестирование — выявляет ошибки взаимодействия между компонентами системы.

  • Функциональное тестирование — имитирует реальные сценарии и выявляет ошибки в бизнес-логике.

  • Тестирование производительности и нагрузки — помогает найти узкие места и ошибки, связанные с производительностью.

  • Ручное тестирование — проверка критических сценариев вручную.

  • Анализ покрытия кода тестами — показывает, какие части кода не покрыты тестами.

  • Статический анализ кода — выявляет сложные участки кода, подверженные ошибкам.

  • Трассировка и логирование — отладка и логи помогают выявить ошибки в работающем коде.

  • Code review — проверка кода опытными разработчиками.

Комбинация этих методов позволяет повысить качество кода и снизить количество ошибок.


Как обеспечить тестирование в БД ?

Вот несколько способов для тестирования кода, работающего с базой данных:

  • Использовать отдельную тестовую БД, отличную от продакшен. Перед тестами накатывать тестовые данные с помощью миграций или фикстур.

  • Транзакции — каждый тест выполнять в отдельной транзакции, которая откатывается по завершении теста. Это изолирует данные тестов друг от друга.

  • Использовать моки для эмуляции работы БД в юнит-тестах без обращения к реальной БД. Например, с помощью PHPUnit можно мокнуть PDO.

  • Фабрики данных — генерировать тестовые данные через factory методы, а не через фикстуры. Это даёт большую гибкость.

  • Snapshot testing — сохранять снапшоты данных в БД после операций и сравнивать с ожидаемым результатом.

  • Использовать библиотеки вроде DBUnit для автоматизации тестирования с базой данных.

  • Настроить очистку тестовых данных после прохождения тестов, чтобы сохранять БД в чистом состоянии.

Главное — изолировать тесты друг от друга и использовать тестовые данные, а не продакшен. Это позволит качественно протестировать работу с БД.


В phpunit есть моки?

Да, в PHPUnit есть возможность создавать мок-объекты (mock objects) для тестирования.

Основные способы создания моков в PHPUnit:

  • Моки для интерфейсов — используя метод createMock() можно создать мок-объект для заданного интерфейса или класса.

  • Заглушки методов — метод willReturn() позволяет задать возвращаемое моком значение для указанного метода.

  • Ожидание вызовов — с помощью expect() и method() можно задать ожидаемые вызовы методов мока.

  • Верификация — метод verify() проверяет, что ожидаемые вызовы произведены.

  • Задание аргументов when() , with() позволяют указать аргументы ожидаемых вызовов.

  • Фиксация моков — функция getMock() сохраняет созданный мок в реестре моков PHPUnit.

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

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


Задачи

№1 : Добавление кэширования

Добавить кэширование в метод getByIds (интерфейс для кеша указан), т.о. чтобы ИД кэшировались и обращение к API шло только если данных нет в кеше.

<?php

/**
 * Интерфейс к сервису кэша (имплементировать класс не нужно)
 */
interface Cache
{
    /**
     * @param int[] $ids
     *
     * @return array<int,Item|null>
     */
    public function get(array $ids): array;

    /**
     * @param array<int,Item> $items
     */
    public function set(array $items): void;
}

/**
 * Интерфейс к http-клиенту сервиса объявлений (имплементировать класс не нужно)
 */
interface Client
{
    /**
     * @param int[] $ids
     *
     * @return array<int,Item>
     */
    public function get(array $ids): array;
}

/**
 * Класс служит обёрткой над http-клиентом к сервису, хранящему информацию об объявлениях.
 * Метод `getByIds`, принимает массив идентификаторов и возвращающий массив найденных сущностей Item.
 * Нужно дополнить метод `getByIds`, чтобы он мог использовать кэш.
 */
class ItemService
{
    public function __construct(private Client $client)
    {
    }

    /**
     * @param int[] $ids
     *
     * @return array<int,Item>
     */
    public function getByIds(array $ids): array
    {
        // @todo Написать логику работы с кешом
        return $this->client->get($ids);
    }
}

Мое решение №1 (не оптимальное):

 class ItemService
{
    private Client;
    private Cache;

    public function __construct(Client $client, Cache $cache)
    {
        $this->client = $client;
        $this->cache = $cache;
    }

    /**
     * @param int[] $ids
     *
     * @return array<int,Item>
     */
    public function getByIds(array $ids): array
    {
        $cahceIds = $this->cache->get($ids);

        $needIds = [];
        foreach($cacheIds as $cid => $data) {
            if($data === null) {
                $res[$cid] = $this->client->get([$cid]);
            }  else {
                $res[$cid] = $data;
            }      
        }

        return $res;
    }
}

(минус) Не оптимально тем, что будет сделано N обращение к API за недостающими данными (а должно быть одно)

Решение №2: Более верное

   /**
     * @param int[] $ids
     * @return array<int,Item>
     */
    public function getByIds(array $ids): array
    {
        $caсheIds = $this->cache->get($ids);

        $needIds = [];
        foreach($cacheIds as $cid => $data) {
            if($data === null) {
                $needIds[] = $cid; 
            }  else {
                $res[$cid] = $data;
            }      
        }

        if(count($needIds)) {
            $needData = $this->client->get($needIds);
            foreach($needData as $key => $data) {
                $res[$key] = $data;
            }
            $this->cache->set($needData);
        }

        return $res;
    }

(warning) Финальное решение тут


Задача №2: чтение большого лога

В коде ниже написать тело метода numberOfErrors т.о., чтобы в нем считалось кол-во ошибок указанного типа в лог-файле. Фишка в том, что файл м.б. большой (много Гб), и скрипт не должен падать из-за лимитов по памяти.

<?php

declare(strict_types=1);

// Некий класс ответа на запрос из нашего фреймворка. Приведен просто для референса
class Response
{
    public function __construct(
        public string $content,
        public int $code
    ) {
    }

    // ...
}

class LogController
{
    /**
     * Метод numberOfErrors возвращает в ответ json, со следующей структурой:
     * {
     *   "found_errors": <int>
     * }
     *
     * Он выполняется максимум X мс, которые мы можем задать в конфиге.
     * Он проходится по файлу log.txt и ищет там ошибки с кодом $errorCode.
     * Он возвращает количество найденных ошибок за период времени.
     *
     * На сервере для PHP процесса выделяется 250 mb памяти. Размер файла log.txt - 10gb
     * Файл расположен в корне (/log.txt), его содержимое:
     * <timestamp>;<error_code>
     */
    #[Route('numberOfErrors/{errorCode}')]
    public function numberOfErrors(int $errorCode): Response
    {
        // написать имплементацию метода

        return new Response('', 200);
    }
}

Мое решение №1: с помощью генераторов

public function numberOfErrors(int $errorCode): Response
    {
        $cnt = 0;
        // написать имплементацию метода
        foreach($this->readLine("log.txt") as $line) {
            $exp = explode(";", $line);
            $errCode = $exp[1] ?? null;
            if($errCode == $errorCode) {
                $cnt++;
            }       
        }

        $json = jsone_encode(['found_errors' => $cnt])

        return new Response($json, 200);
    }

    private function readLine(string $fname) 
    {
        if(!file_exists($fname)) {
            return null;
        }

        if($this->f === null) {
            $this->f = fopen($fname, "r");
        }

        while(!feof($this->f)) {
            $str = fgets($f);       // read 
            yield $str;
        }

        fclose($this->f);
    }
}

Решение №2: (поправленное после обсуждений)

(звезда) Генератор тут не нужен, это over-engineering

    public function numberOfErrors(int $errorCode): Response
    {
        $cnt = 0;

        $fname = self::FNAME;

        if(!file_exists($fname)) {
            $json = json_encode(['error' => 'file not found']);
            return new Response($json, 500);
        }

        if($this->f === null) {
            $this->f = fopen($fname, "r");
        }

        // написать имплементацию метода
        while(!feof($this->f)) {
            $line = fgets($this->f);       // read new line
            $exp = explode(";", $line);
            $errCode = $exp[1] ?? null;
            if($errCode == $errorCode) {
                $cnt++;
            }
        }

        fclose($this->f);

        $json = json_encode(['found_errors' => $cnt]);

        return new Response($json, 200);
    }

(звезда) Доп. вопрос: как выдать результат спустя 100 микро-сек запуска ?

То есть если сервером задано ограничение на время запуска скрипта в ~100 мсек, и нельзя его превысить, и нужно выдать результат (кол-во посчитанных ошибок) — сколько успели посчитать за это время.

Мое первое предположение : использовать файберы? — “нет, слишком сложно” (хотя это то что надо, имхо)

(плюс) Решение: можем считать время работы скрипта и выходить из цикла чтения спустя Х мсек (верно):

        $ts = (microtime(true));

        // написать имплементацию метода
        while(!feof($this->f)) {
            $line = fgets($this->f);       // read new line
            $exp = explode(";", $line);
            $errCode = $exp[1] ?? null;
            if($errCode == $errorCode) {
                $cnt++;
            }

            // script execution time limit
            $time = (microtime(true) - $ts);
            if($time >= self::TIMEOUT) {
                break;
            }
        }

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

if($time >= app::getConfig()->getVar('exec_timeout')) { ...

(warning) Финальное решение тут


Интервью далее в Авито

  • HR

  • Техн. скриннинг

  • Live-кодинг

  • Платформа (была сейчас)

  • Алгоритмы — на моем языке

  • Архитектура / System design — на senior

  • Софт скиллы: знакомство с командой, product-owner


Рекомендую для прокачки

(warning) Скачайте мою книгу по подготовке к собеседованиям веб-разработчика. Здесь подробно разобрано множество вопросов и задач с интервью, накопленные мной за 10+ лет опыта.

Tags

Нет Ответов

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

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

Рубрики


Подпишись на новости
👋

Есть вопросы?