Привет, Хабр! Меня зовут Арсений, я — тимлид в команде разработки инструментов разработчика KasperskyOS. Работа нашей команды заключается в том, чтобы делать жизнь разработчика ПО под нашу собственную микроядерную OS удобной, так что любые технологии, упрощающие жизнь разработчика, не оставляют нас равнодушными. Вместе со всеми мы следим за хайпом вокруг нейросетей и решили сделать небольшой обзор AI-плагинов автодополнения кода, которые каждый из нас может использовать уже сейчас.


image


В этой заметке попробуем сравнить следующие AI плагины VSCode:


  • Copilot v1.84.61 — самый нашумевший робот
  • Tabnine v3.6.45 — самый старый из трех и самый дорогой
  • Codeium v1.2.11 — самый свежий и самый малоизвестный
  • FauxPilot — Open source, self-hosted аналог Copilot, использующий модели от CodeGen; посмотрим, что может противопоставить коммерческим продуктам OSS-проект, развернутый на моем запечном сервере.

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


Условия


  • В основном будем писать на Python и изредка С++.
  • Делаем упор именно на функцию автодополнения (у некоторых рассматриваемых плагинов есть и другие функции, их оставим в стороне).
  • В качестве редактора возьмем VSCode.
  • Проводим каждый эксперимент в отдельном проекте, чтобы результаты не влияли друг на друга.
  • FauxPilot: Модель codegen-2B-multi, используемая видеокарта — GeForce GTX 1080, 8gb vram, VSCode расширение — FauxPilot. О настройках — Отдельный раздел.
  • Прощаем FauxPilot большие задержки при кодогенерации, так как это скорее проблема моего сервера.

Disclaimer 1 — Как и всегда с нейронками, результат их работы плохо воспроизводим, так как зависит от контекста проекта, версии нейронки, фазы Луны и так далее. Так что не все результаты из этой статьи можно в точности воспроизвести, где-то результаты будут хуже где-то лучше, где-то просто другие, имейте это в виду.


Disclaimer 2 — Статья получилась больше чем я хотел, так что для тех кто спешит, вот ссылки на самые интересные разделы (где разница между роботами наиболее сильна):



Простые функции


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


Перемешаем символы в строке


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


Copilot


def shuffle_string(input: str) -> str:
=== далее код от copilot: ===
    """Shuffle a string."""
    return "".join(random.sample(input, len(input)))

Tabnine


def shuffle_string(input: str) -> str:
=== далее код от tabnine: ===
    return "".join(sorted(input))

Совсем не то, поможем комментарием.


def shuffle_string(input: str) -> str:
    """
    Shuffle the letters of a string
    :param input: string to shuffle
    :return: shuffled string
    """
=== далее код от tabnine: ===
    return "".join(random.sample(input, len(input)))

Вот, теперь другое дело.


Codeium


def shuffle_string(input: str) -> str:
=== далее код от codeium: ===
    return ''.join(random.sample(input, len(input)))

Все так же хорошо.


FauxPilot


import random

def shuffle_string(input: str) -> str:
    """
    Shuffle the letters of a string
    :param input: string to shuffle
    :return: shuffled string
    """
=== далее код от fauxpilot: ===    
    return ''.join(random.sample(input, len(input)))

В данном случае помимо комментария пришлось добавить import random, но код получился корректный!


Случайная дата в диапазоне


Copilot


def get_random_datetime(from_date: datetime, to_date: datetime) -> datetime:
=== далее код от copilot: ===
    """
    Get a random datetime between two datetime objects
    :param from_date: start datetime
    :param to_date: end datetime
    :return: random datetime
    """
    import random
    return from_date + (to_date - from_date) * random.random()

Tabnine


def get_random_datetime(from_date: datetime, to_date: datetime) -> datetime:
=== далее код от tabnine: ===
    """
    Get a random datetime between two datetime objects
    :param from_date: start datetime
    :param to_date: end datetime
    :return: random datetime
    """
    return from_date + timedelta(days=random.randint(0, (to_date - from_date).days))

В целом ок, хотя случайной будет только дата, а не время, но придираться не будем.


Codeium


def get_random_datetime(from_date: datetime, to_date: datetime) -> datetime:
=== далее код от codeium: ===
    return from_date + timedelta(days=random.randint(0, (to_date - from_date).days))

Аналогично


FauxPilot


def get_random_datetime(from_date: datetime, to_date: datetime) -> datetime:
=== далее код от fauxpilot: ===
    """
    Return a random datetime between from_date and to_date.
    """
    return from_date + datetime.timedelta(seconds=random.randint(0, int((to_date - from_date).total_seconds())))

Неплохо! Признаюсь, не с первого раза и не сразу, потребовался тюнинг. Чтобы не перегружать каждый раздел, я собрал все оговорки и нюансы, касающиеся FauxPilot, в последний раздел этой статьи. Они есть и немаловажные, но для краткости собрал их все в одном месте.


Раздел Copilot Tabnine Codeium FauxPilot
Генерация простых функций

Считаем, что квалификация пройдена, пойдем дальше.


Чуть более сложные примеры


Парсинг строки по заданному в комментариях формату


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


Copilot


def iteration_string_to_dates_range(iteration_string: str) -> tuple[date, date]:
    """  
    Convert iteration_string string to range of dates
    Args:
        iteration_string - string with format "SomeProject\yyyy\yyyy:MM:dd - yyyy:MM:dd" 
    Returns:
        tuple of dates, extracted from iteration_string
    """
=== далее код от copilot: ===    
    dates = iteration_string.split("\\")[-1].split("-")
    start_date = datetime.strptime(dates[0].strip(), "%Y:%m:%d").date()
    end_date = datetime.strptime(dates[1].strip(), "%Y:%m:%d").date()
    return start_date, end_date

Отлично.


Tabnine сгенерировал похожий, но совершенно некорректный код, как с дополнительными подсказками, так и без них.


def iteration_string_to_dates_range(iteration_string: str) -> tuple[date, date]:
    """  
    Convert iteration_string string to range of dates
    Args:
        iteration_string - string with format "SomeProject\yyyy\yyyy:MM:dd - yyyy:MM:dd" 
    Returns:
        tuple of dates, extracted from iteration_string
    """
=== далее код от tabnine: ===
    date_range = iteration_string.split(" - ")
    start_date = datetime.strptime(date_range[0], "%Y-%m-%d").date()
    end_date = datetime.strptime(date_range[1], "%Y-%m-%d").date()
    return start_date, end_date

Codeium не сгенерировал ничего вразумительного.


FauxPilot


def iteration_string_to_dates_range(iteration_string: str) -> tuple[date, date]:
    """  
    Convert iteration_string string to range of dates
    Args:
        iteration_string - string with format "SomeProject\yyyy\yyyy:MM:dd - yyyy:MM:dd" 
    Returns:
        tuple of dates, extracted from iteration_string
    """
    iteration_string_split = iteration_string.split(" - ")
    iteration_date_start = datetime.strptime(iteration_string_split[0], "%Y:%m:%d")
    iteration_date_end = datetime.strptime(iteration_string_split[1], "%Y:%m:%d")
    return (iteration_date_start, iteration_date_end)

Та же ошибка что и у Tabnine.


Генерируем регулярное выражение


Ну кто любит вспоминать эти странные закорючки, пусть с этим мается машина. Попросим вырезать имя пользователя из Apache-лога.


Copilot


# A regular expression to extract the username from a log string in this format. In this example, the username is "frank".
EXAMPLE_LOG_STRING = " 127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] \"GET /apache_pb.gif HTTP/1.0\" 200 2326 "
USERNAME_REGEXP = r'\s-\s(\w+)\s\['

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


Tabnine молчит, Сodeium и FauxPilot сгенерировали неверный регексп.


Подбор подходящего исключения


Проверим способность подобрать подходящее исключение по контексту.


Copilot


import click
from click.core import ParameterSource

def docker_run(ctx: click.Context):
    restart = ctx.params.get("restart")
    rm = ctx.params.get("rm")
    if rm and restart:
=== далее код от tabnine: ===
        raise click.UsageError(
            "You can't use --rm and --restart together",
            ctx=ctx,
            param=ctx.command.params["restart"],
            param_hint="restart",
        )

Tabnine


import click
from click.core import ParameterSource

def docker_run(ctx: click.Context):
    restart = ctx.params.get("restart")
    rm = ctx.params.get("rm")
    if rm and restart:
=== далее код от tabnine: ===
        raise click.UsageError(
            "Cannot specify both --rm and --restart"
        )

Codeium


import click
from click.core import ParameterSource

def docker_run(ctx: click.Context):
    restart = ctx.params.get("restart")
    rm = ctx.params.get("rm")
    if rm and restart:
=== далее код от tabnine: ===
        ctx.fail("Cannot use --rm and --restart together")

Тоже нормально.


FauxPilot


def docker_run(ctx: click.Context):
    restart = ctx.params.get("restart")
    rm = ctx.params.get("rm")
    if rm and restart:
=== далее код от fauxpilot: ===
        raise click.UsageError("--rm and --restart are mutually exclusive")
    if not (rm or restart):
        raise click.UsageError("--rm or --restart is required")

Нафантазировал лишнее (см. оговорки в последнем разделе), но результат ок.


Конвертация структур данных


В проекте, использующем python-объекты в C++ коде, попросим собрать структуру из соответствующего ей PyObject.


Copilot


#include <Python.h>

struct BuildInfo {
    std::string version;
    std::string commit;
    std::string date;
};

// convert python dataclass to C++ struct
=== далее код от copilot: ===
BuildInfo getBuildInfo(PyObject* buildInfo) {
    BuildInfo info;
    PyObject* version = PyObject_GetAttrString(buildInfo, "version");
    PyObject* commit = PyObject_GetAttrString(buildInfo, "commit");
    PyObject* date = PyObject_GetAttrString(buildInfo, "date");

    info.version = PyUnicode_AsUTF8(version);
    info.commit = PyUnicode_AsUTF8(commit);
    info.date = PyUnicode_AsUTF8(date);

    Py_DECREF(version);
    Py_DECREF(commit);
    Py_DECREF(date);

    return info;
}

Tabnine


#include <Python.h>

struct BuildInfo {
    std::string version;
    std::string commit;
    std::string date;
};

// convert python dataclass to C++ struct
BuildInfo getBuildInfo(PyObject* buildInfo) {
=== далее код от tabnine: ===
    BuildInfo info;

    PyObject* version = PyObject_GetAttrString(buildInfo, "version");
    PyObject* commit = PyObject_GetAttrString(buildInfo, "commit");
    PyObject* date = PyObject_GetAttrString(buildInfo, "date");

    if (version == NULL || commit == NULL || date == NULL) {
        PyErr_Print();
        return info;
    }

    info.version = std::string(PyString_AsString(version));
    info.commit = std::string(PyString_AsString(commit));
    info.date = std::string(PyString_AsString(date));

    Py_DECREF(version);
    Py_DECREF(commit);
    Py_DECREF(date);

    return info;
}

Copilot и Tabnine справляются одинаково хорошо.


Codeium


BuildInfo getBuildInfo(PyObject* buildInfo) {
    BuildInfo info;
    info.version = PyUnicode_AsUTF8(PyDict_GetItemString(buildInfo, "version"));
    info.commit = PyUnicode_AsUTF8(PyDict_GetItemString(buildInfo, "commit"));
    info.date = PyUnicode_AsUTF8(PyDict_GetItemString(buildInfo, "date"));
    return info;
}

Есть нюансы, но допустим.


FauxPilot



BuildInfo getBuildInfo(PyObject* buildInfo) 
{
    BuildInfo buildInfoCpp;
    PyObject* version = PyObject_GetAttrString(buildInfo, "version");
    PyObject* commit = PyObject_GetAttrString(buildInfo, "commit");
    PyObject* date = PyObject_GetAttrString(buildInfo, "date");
    if (version == NULL || commit == NULL || date == NULL) {
        return buildInfoCpp;
    }
    buildInfoCpp.version = PyUnicode_AsUTF8(version);
    buildInfoCpp.commit = PyUnicode_AsUTF8(commit);
    buildInfoCpp.date = PyUnicode_AsUTF8(date);
    return buildInfoCpp;
}

Допустим.


Итак, полноценно раздел прошел только Copilot, для остальных мы нащупали предел. При этом стоит отметить, что FauxPilot в целом держится молодцом! За исключением значительно больших задержек при кодогенерации, конечно, но тут вопросы к железу скорее.


Раздел Copilot Tabnine Codeium FauxPilot
Генерация более сложного кода   ❌✅ ❌✅ ❌✅



Генерация тестов


Тест на простую функцию


Перейдем к смежной теме — попробуем проверить способность генерировать тесты на существующие функции.


Возьмем библиотеку toolz (набор полезной функциональщины для python)
Удаляем все существующие тесты (это важно).


Попробуем сгенерировать какой-то общий тест на функцию drop — принимающую последовательность и число n, и возвращающую последовательность с пропущенными n первыми элементами.


Copilot


def test_drop():
=== далее код от copilot: ===
    assert list(drop(2, [1, 2, 3, 4])) == [3, 4]
    assert list(drop(0, [1, 2, 3, 4])) == [1, 2, 3, 4]
    assert list(drop(10, [1, 2, 3, 4])) == []
    assert list(drop(2, [])) == []
    assert list(drop(0, [])) == []

Tabnine


def test_drop():
=== далее код от Tabnine: ===
    assert list(drop(2, [10, 20, 30, 40, 50])) == [30, 40, 50]
    assert list(drop(3, [10, 20, 30, 40, 50])) == [40, 50]
    assert list(drop(4, [10, 20, 30, 40, 50])) == [50]

Тест похуже, но опустим нюансы.


Codeium


def test_drop():
=== далее код от Codeium: ===
    assert list(drop(2, [10, 20, 30, 40, 50])) == [30, 40, 50]

Аналогично.


FauxPilot


def test_drop():
    assert tuple(drop(2, [10, 20, 30, 40, 50])) == (30, 40, 50)

Аналогично.


Учет в тесте содержимого функции


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


def asdfasdfkajsdsd(input_data: List[int], count: int) -> int:

    if len(input_data) == 0:
        raise ValueError("Empty data")

    if count <= 0:
        raise MemoryError("count must be positive")

    ...

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


Copilot


def test_asdfasdfkajsdsd_raises():
=== далее код от copilot: ===
    with pytest.raises(SyntaxError):
        asdfasdfkajsdsd([], 1)

    with pytest.raises(MemoryError):
        asdfasdfkajsdsd([1, 2, 3], 0)

Хороший результат.


Tabnine


def test_asdfasdfkajsdsd_raises():
=== далее код от tabnine: ===
    with pytest.raises(SyntaxError):  
        asdfasdfkajsdsd([], 0)      

Вроде понимает что я хочу, но не до конца генерирует тест. Возможно, баг, а может, и правда не увидел второе исключение?


Codeium


def test_asdfasdfkajsdsd_raises():
=== далее код от Codeium: ===
    with pytest.raises(ValueError):
        asdfasdfkajsdsd([], 0)
    with pytest.raises(MemoryError):
        asdfasdfkajsdsd([], -1)

Пришлось немного попинать, но сгенерировал.


FauxPilot


def test_asdfasdfkajsdsd_raises():

    with pytest.raises(ValueError):
        asdfasdfkajsdsd([1, 2, 3], -1)

    with pytest.raises(MemoryError):
        asdfasdfkajsdsd([1, 2, 3], 4)

Близко, но не совсем.


Будем считать, что все более или менее прошли.


Раздел Copilot Tabnine Codeium FauxPilot
Генерация тестов ❌✅




Понимание контекста проекта


Это очень важный раздел, фактически самый важный, но обойдемся без лишних примеров ради краткости. Тут достаточно сказать сразу, что, на удивление, только Copilot умеет смотреть в соседние файлы. Tabnine и Codeium смотрят только в текущий открытый файл, что конечно драматически сказывается на учитываемом нейронкой контексте.


Простой пример, почему это важно:

1) Возьмем два проекта — менеджер процессов и шутер с зомби.
2) Попробуем написать в новом пустом файле (это важно) функцию kill_zombie
3) Copilot в одном случае будет убивать процесс, в другом — агрессивного мертвеца, остальные же в обоих проектах будут генерировать одинаковый код.


До того, как Copilot научился смотреть в другие файлы, практиковались разные лайфхаки, например временно вставить в начало текущего файла содержимое всех остальных :) Сейчас это можно использовать в Tabnine/Codeium, но это конечно не решает проблему.


В этот раз результат однозначен.


Раздел Copilot Tabnine Codeium FauxPilot
Понимание контекста проекта




ChatGPT в вашем IDE


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


Поиск нужного хедера


В качестве примера попробуем отыскать нужный хедер, не выходя в Интернет с таким вопросом:


Copilot


// header with get_time
=== далее код от copilot: ===
#include <iomanip>

Tabnine


// header with get_time
=== далее код от tabnine: ===
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <algorithm>
#include <cmath>

Что-то да подойдет?) В общем, тут провал.
Вообще, Tabnine как правило либо игнорирует такие промты, либо генерирует что-то не то.


Codeium


// header with get_time
=== далее код от codeium: ===
#include <iostream>

Тоже мимо.


Вытягиваем значения общеизвестных констант


Попробуем создать несколько констант, пишем только имя константы, остальное ожидаем от робота.
Copilot


UNIX_FIRST_RELEASE_YEAR = 1969
GAGARIN_FLIGHT_DATE = "12.04.1961"
HUMAN_BONES_COUNT = 206

Вроде все верно.


Tabnine ничего не генерирует.


Codeium


UNIX_FIRST_RELEASE_YEAR = 2022
GAGARIN_FLIGHT_DATE = "2022-01-01"
HUMAN_BONES_COUNT = 0

Лучше бы молчал :)


Генерируем текст


Попробуем развить тему промтов, и посмотрим кто как с этим справляется.


Погенерируем текст на любую тему.
Copilot


/*
Pros of Monorepos:
=== далее код от copilot: ===
- Easier code reuse
- Easier dependency management
- Atomic changes
- Easier code review
...
Cons of Monorepos:
=== далее код от copilot: ===
- Harder to setup
- Harder to maintain
- Harder to scale
- Harder to build
*/

В конце концов он может генерировать любой текст.
Copilot


/*
A little poem about a bee hive that tried to adopt a Monorepo approach:

=== далее код от copilot: ===

There once was a hive that was very large
It had many bees, and they were in charge
They all worked together, and they all worked hard
But they couldn't agree on a single shard
So they split up the hive into many small hives
And each bee was happy, and each bee thrived
But then one day, a bear came along
And ate all the bees, and it was all wrong
So the bear went away, and the bees came back
And they all worked together, and they all worked hard
And they all agreed on a single shard
And they all lived happily ever after
The end
*/

Не знаю, что именно, но что-то он хочет нам сказать!


Tabnine тут тоже ничего не генерирует.


Codeium


/*
Pros of Monorepos:
=== далее код от Codeium: ===
- Single source of truth
- Single source of truth
- Single source of truth
...

Cons of Monorepos:
=== далее код от Codeium: ===
- Single source of truth
- Single source of truth
- Single source of truth
...
*/

В общем, тексты он тоже не особо генерирует.


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


Переводчик человеческих языков


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


Copilot


/*
And they all worked together, and they all worked hard
The same phrase in Russian, Italian, Greek and Mandarin:
=== далее код от copilot: ===
Все работали вместе, и все работали усердно
Tutti lavoravano insieme, e tutti lavoravano sodo
Όλοι δούλευαν μαζί, και όλοι δούλευαν σκληρά
大家一起工作,大家都很努力
*/

С Codeium и Tabnine такого повторить не удалось. FauxPilot мучить тоже тут я не стал.


Раздел Copilot Tabnine Codeium FauxPilot
ChatGPT в вашем IDE




Рутина вне кодинга


Одна из самых актуальных для робота задач — генерация рутинного кода, и не только кода. Раздел на самом деле плотно связан с предыдущим, и работает тут только Copilot, приведем пример.
Возьмем написание файла docker-compose.yaml на основе уже написанной команды docker run.
В начало пустого docker-compose.yaml вставим нашу готовую команду, напишем затравку в виде строки version: 2.1, а дальше ждем робота


Copilot


# docker run -d \
#   --name=vpn \
#   -e PUID=1000 \
#   -e PGID=1000 \
#   -v ~/vpn/config:/config \
#   -v /vpn/lib/modules:/lib/modules \
#   -p 666:666/udp \
#   --cap-add=NET_ADMIN \
#   --cap-add=SYS_MODULE \
#   --sysctl net.ipv4.conf.all.src_valid_mark=1 \
#   --restart unless-stopped \
#   som_vpn_image

version: "2.1"
=== далее код от copilot: ===
services:
  vpn:
    image: som_vpn_image
    container_name: vpn
    environment:
      - PUID=1000
      - PGID=1000
    volumes:
      - ~/vpn/config:/config
      - /vpn/lib/modules:/lib/modules
    ports:
      - 666:666/udp
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    sysctls:
      - net.ipv4.conf.all.src_valid_mark=1
    restart: unless-stopped

Сработало хорошо. В обратную сторону — docker-compose.yaml -> docker run ..., работает тоже хорошо, кстати.
Попробуем Tabnine.


Tabnine


# docker run -d \
#   --name=vpn \
#   -e PUID=1000 \
#   -e PGID=1000 \
#   -v ~/vpn/config:/config \
#   -v /vpn/lib/modules:/lib/modules \
#   -p 666:666/udp \
#   --cap-add=NET_ADMIN \
#   --cap-add=SYS_MODULE \
#   --sysctl net.ipv4.conf.all.src_valid_mark=1 \
#   --restart unless-stopped \
#   som_vpn_image

version: "2.1"
=== далее код от tabnine: ===
platform: "Linux"
platform_version: "2.1"
platform_requirements:
  - ">=0.6.0"

Совсем мимо :)


Поможем железяке, начнем писать сервис.


# docker run -d \
#   --name=vpn \
#   -e PUID=1000 \
#   -e PGID=1000 \
#   -v ~/vpn/config:/config \
#   -v /vpn/lib/modules:/lib/modules \
#   -p 666:666/udp \
#   --cap-add=NET_ADMIN \
#   --cap-add=SYS_MODULE \
#   --sysctl net.ipv4.conf.all.src_valid_mark=1 \
#   --restart unless-stopped \
#   som_vpn_image

version: "2.1"
services:
  vpn:
=== далее код от tabnine: ===
    - name: vpn  
    version: "2.1"  
    services:

Wat?) Увы, тест провален окончательно.


Codeium молчит, FauxPilot бредит.


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


Раздел Copilot Tabnine Codeium FauxPilot
Рутина вне кода



Рефакторинг


Глубокая вложенность


Напишем какую-то переусложненную функцию, посмотрим, сможет ли ее отрефакторить AI.


Copilot


# too nested code
def print_page(data):
    if data.input_staus == InputStatus_Success:
        page = SetupPage()
        if page:
            if page.Fill(data):
                printer = GetPrinter()
                if printer.Ready():
                    status = printer.PrintPage(page)
                    if status == PrinterStatus_Success:
                        return True
    return False

# refactored print_page() function with less nesting:
def print_page_less_nested(data):
=== далее код от copilot: ===
    if data.input_staus != InputStatus_Success:
        return False

    page = SetupPage()
    if not page:
        return False

    if not page.Fill(data):
        return False

    printer = GetPrinter()
    if not printer.Ready():
        return False

    status = printer.PrintPage(page)
    if status != PrinterStatus_Success:
        return False

    return True

Tabninie


# too nested code
def print_page(data):
    if data.input_staus == InputStatus_Success:
        page = SetupPage()
        if page:
            if page.Fill(data):
                printer = GetPrinter()
                if printer.Ready():
                    status = printer.PrintPage(page)
                    if status == PrinterStatus_Success:
                        return True
    return False

# refactored print_page() function with less nesting:
def print_page_less_nested(data):
=== далее код от tabnine: ===
    if data.input_staus == InputStatus_Success:
        page = SetupPage()
        if page:
            if page.Fill(data):
                printer = GetPrinter()
                if printer.Ready():
                    status = printer.PrintPage(page)
                    if status == PrinterStatus_Success:
                        return True
    return False

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


Codeium


def print_page(data):
    if data.input_status != InputStatus_Success:
        return False

    page = SetupPage()
    if not page:
        return False

    if not page.fill(data):
        return False

    printer = get_printer()
    if not printer.ready():
        return False

    if printer.print_page(page) != PrinterStatus_Success:
        return False

    return True

У Codeium есть отдельная кнопка — Refactor, которая делает то что нам нужно.


FauxPilot просто продублировал старую функцию.


Модернизация кода


Наверное сыроватый эксперимент, но попробуем написать класс C++ с большим количеством анахронизмов, затем попробуем заставить AI переписать его в более современном стиле.


// Old style C++ class
class OldStyleClass {
public:
    OldStyleClass () {
        m_name = NULL;
    }
    void some_function(const void* name) {
        if (name == NULL) {
            cout << "name is NULL" << endl;
        } else {
            m_name = new std::string((const char*)name);
        }
    }

    void some_function_2() {
        int arr[4];
        arr[0] = 1;
        arr[1] = 2;
        arr[2] = 3;
        arr[3] = 4;
        m_name = new std::string((const char*)arr);
    }
private:
    OldStyleClass& operator=(const OldStyleClass& rhs);
    std::string* m_name;
};

// New style C++ class refactored with modern features
class NewFancyStyleClass {
=== далее код от copilot: ===
public:
    NewFancyStyleClass () = default;
    void some_function(const void* name) {
        if (name == nullptr) {
            cout << "name is nullptr" << endl;
        } else {
            m_name = std::make_unique<std::string>((const char*)name);
        }
    }

    void some_function_2() {
        int arr[4] = {1, 2, 3, 4};
        m_name = std::make_unique<std::string>((const char*)arr);
    }
private:
    std::unique_ptr<std::string> m_name;
};

Толком не знаю, чего я от него хотел добиться. Но вроде от NULL он избавился, default использовал, умный указатель притащил, инициализацию массива отрефакторил. Наверное примерно этого и хотел. :)


Tabnine, Codium и FauxPilot не поняли чего я от них хочу, просто скопировали OldStyleClass один в один.


Раздел Copilot Tabnine Codeium FauxPilot
Рефакторинг ✅❌



Генерация комментариев


Попробуем сгенерировать комментарий к функции по ее коду. Причем одна функция будет иметь говорящее название, другая нет.


Функция имеет говорящее название


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


/*
toHTML
=== далее коммент от copilot: ===
@description Converts the color to a string in HTML format.
@param format The format to use.
@return The color as a string in HTML format.
@throws std::runtime_error If the format is not recognized.
*/
std::string
Color::toHTML( Format format ) const
{
    float w, x, y, z;
    if ( format == RGBA ) {
        w = r(), x = g(), y = b(), z = a();
    }
    if ( format == ABGR ) { // ABGR
        w = a(), x = b(), y = g(), z = r();
    }
    else 
    {
        throw std::runtime_error("Unrecognized color format");
    }

    return Stringify()
        << "#"
        << std::hex << std::setw(2) << std::setfill('0') << (int)(w*255.0f)
        << std::hex << std::setw(2) << std::setfill('0') << (int)(x*255.0f)
        << std::hex << std::setw(2) << std::setfill('0') << (int)(y*255.0f)
        << std::hex << std::setw(2) << std::setfill('0') << (int)(z*255.0f);
}

Tabnine снова отказался что-то генерировать, как бы я ни старался. FauxPilot тоже.


Codeium


/**
 * Convert the color to its HTML representation.
 * @param format The format of the color: RGBA or ABGR.
 * @return A string containing the HTML representation of the color.
 * @throws std::runtime_error if an unrecognized color format is given.
 */

У Сodeium есть специальная кнопка для генерирования документации, сработала хорошо.


Функция НЕ имеет говорящего названия


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


def asdfsfadsf(*seqs, **kwargs):
    N = len(seqs)
    if N == 1 and isinstance(seqs[0], list):
        seqs = seqs[0]
        N = len(seqs)
    if N < 2:
        raise ValueError('adfadsf')
    default = kwargs.get('default', no_default)
    if default == no_default:
        iters = zip(*seqs)
    else:
        iters = zip_longest(*seqs, fillvalue=default)
    key = kwargs.get('key', None)
    if key is None:
        for items in iters:
            if items.count(items[0]) != N:
                yield items
    else:
        for items in iters:
            vals = tuple(map(key, items))
            if vals.count(vals[0]) != N:
                yield items

Copilot


    """
    asdfsfadsf
    This function is used to find the intersection of multiple sequences.
    :param seqs: multiple sequences
    :param kwargs: default, key
    :raises ValueError: if the number of sequences is less than 2
    :yield: the intersection of multiple sequences
    """

Комментарий неверный.


Codeium


    """
    This function takes in multiple sequences and returns a generator object
    that yields the items that are not equal across all sequences.
    :param seqs: one or more sequences
    :type seqs: tuple
    :param default: the default value to use when sequences are of different
    lengths
    :type default: any
    :param key: a function that returns the value to compare for each item in
    the sequences
    :type key: function
    :raises ValueError: if less than 2 sequences are provided
    :yields: items that are not equal across all sequences
    """

Комментарий верный!
FauxPilot тут завести не удалось.


Раздел Copilot Tabnine Codeium FauxPilot
Генерация комментариев ✅❌



Специфичные сценарии


Работа в системе сборки


Возьмем наш пример с Python-биндингами в С++ коде и попробуем сгенерировать cmake-файл для него.


Тут без промта никак.


Copilot


# CMakeLists.txt content for build main.cpp into a single executable file named HelloWorld. Link with libpython, include python headers:
cmake_minimum_required(VERSION 3.0)
=== далее код от copilot: ===
project(HelloWorld)
find_package(PythonLibs 3.5 REQUIRED)
include_directories(${PYTHON_INCLUDE_DIRS})
add_executable(HelloWorld main.cpp)
target_link_libraries(HelloWorld ${PYTHON_LIBRARIES})

Генерирует нормально, все собирается.


Попробуем добавить пару флагов дополнительных целей для удобства работы:


# build with address sanitizer
=== далее код от copilot: ===
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer")

# add extra make goal for running the executable
=== далее код от copilot: ===
add_custom_target(run
    COMMAND HelloWorld
    DEPENDS HelloWorld
    WORKING_DIRECTORY ${CMAKE_PROJECT_DIR}
)

# goal to remove all files and folders in the system root
=== далее код от copilot: ===
add_custom_target(clean_root
    COMMAND rm -rf /*
    WORKING_DIRECTORY /
)

Работает!


Tabnine
Участник соревнований от комментариев отказался, ничего вразумительного не сгенерировал.


Codeium


# CMakeLists.txt content for build main.cpp into a single executable file named HelloWorld. Link with libpython, include python headers:
cmake_minimum_required(VERSION 3.5)
project(HelloWorld)
find_package(PythonLibs REQUIRED)
include_directories(${PYTHON_INCLUDE_DIRS})
add_executable(HelloWorld main.cpp)
target_link_libraries(HelloWorld ${PYTHON_LIBRARIES})

# build with address sanitizer
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address")

# add extra make goal for running the executable
add_custom_target(run
    COMMAND ./HelloWorld
)

# goal to remove all files and folders in the system root
add_custom_target(clean
    COMMAND rm -rf *
)

Ура, почти все то же самое.


FauxPilot (пришлось слегка подкрутить параметр temperature под задачу )


# CMakeLists.txt content for build main.cpp into a single executable file named HelloWorld. Link with libpython, include python headers:
cmake_minimum_required(VERSION 2.8.12)
project(HelloWorld)
add_executable(HelloWorld main.cpp)
target_link_libraries(HelloWorld python)
install(FILES main.cpp ${PYTHON_INCLUDE_DIR})
install(FILES ${PYTHON_INCLUDE_DIR}/libpython2.7.so)
install(FILES ${PYTHON_INCLUDE_DIR}/libpython3.3.so)

В целом небесполезен.


Перевод из одного языка в другой


Довольно сложный сценарий


В небольшом проекте попробуем переписать один алгоритм в функциональном стиле с использованием библиотеки pipe. Чтобы показать что мы от него хотим, напишем рядом код на С#.


Copilot


... много императивного кода
"""
C# Code 
violations = file_rows.Select(PlanePos.FromRow)
                      .Where(pos => pos.InPolygon(restrictedArea))
                      .OrderBy(pos => pos.Lat)
                      .ThenBy(pos => pos.Time)
                      .Select(pos => $"{pos.Time}: {pos.Lat}-{pos.Lon}")
"""

# Python Code using pipe module
=== далее код от copilot: ===
violations = (file_rows |
              map(PlanePos.from_row) |
              filter(lambda pos: pos.in_polygon(restrictedArea)) |
              sorted(key=lambda pos: (pos.lat, pos.time)) |
              map(lambda pos: f"{pos.time}: {pos.lat}-{pos.lon}") |
              take_while(lambda pos: pos.time < t_end) |
              list)

Похоже на правду


То есть Copilot сработал как переводчик с одного языка программирования на другой. Наверное это мало кому нужно, так что не буду растягивать тут статью, скажу только что почва плодотворная, Сopilot действительно делает это удовлетворительно. Впрочем неудивительно, что же может быть лучшим описанием желаемого кода чем такой-же код, но на другом языке.


Tabnine и Codeium почему-то ничего толкового не сгенерировали, возможно нужно развить тему на примерах попроще, но так как запрос специфический то пропустим.


Раздел Copilot Tabnine Codeium FauxPilot
Специфичные сценарии ❌✅




Общая информация о настройке FauxPilot


Работа FauxPilot критично зависит от настроек в плагине, и постоянно приходится искать баланс между их значениями.
Самые важные две — temperature и Inline completion.


Подробнее о моих нстройках FauxPilot
  • temperature: 0.2 (в паре экспериментов менял на 0.4)
  • max_tokens: 200
  • Max Lines: 150
  • FauxPilot: Inline Completion: false (хотя для реальной работы включил бы true)

Temperature


Параметр отвечает за «креативность». Чем значения ниже, тем осторожнее робот.
Значение по умолчанию — 0.1, на мой взгляд это слишком низко, при таких значениях часто код вообще не генерируется. Эксперименты показывают, что значения 0.2—0.4 оптимальны. При значениях ближе к 1 код будет довольно бесполезный, при значениях больше 1 будет случайный бессмысленный текст. В итоге параметр, в зависимости от задачи, приходится менять, так как где-то не хватает осторожности, а где-то — креативности.


Inline Completion


Эта настройка отвечает за то, чтобы дополнять только текущую строку, и по умолчанию она включена по двум простым причинам:
1) Время работы драматически возрастает для многострочного дополнения, моя 1080 не прокручивает его за приемлемое для написания кода время, иногда приходится ждать секунд 5.
2) В этом режиме FauxPilot добавляет очень много лишнего кода помимо ожидаемого. Например полный сниппет сгенерированной функции получения случайной даты в диапазоне выглядел вот так


def get_random_datetime(from_date: datetime, to_date: datetime) -> datetime:
    """
    Get a random datetime between two datetime objects
    :param from_date: start datetime
    :param to_date: end datetime
    :return: random datetime
    """
=== далее код от fauxpilot: ===  
    return from_date + random.randint(0, (to_date - from_date).total_seconds())

def get_random_date(from_date: datetime, to_date: datetime) -> datetime:
    """
    Get a random datetime between two datetime objects
    :param from_date: start datetime
    :param to_date: end datetime
    :return: random datetime
    """
    return from_date + random.randint(0, (to_date - from_date).total_seconds())

def get_random_time(from_time: datetime, to_time: datetime) -> datetime:
    """
    Get a random datetime between two datetime objects
    :param from_time: start datetime
    :param to_time: end datetime
    :return: random dat

То есть помимо одной нужной строки приехало еще две левых функции.



Выводы


Что ж, подведем итоги.


Раздел Copilot Tabnine Codeium FauxPilot
Генерация простых функций
Генерация более сложного кода   ❌✅ ❌✅ ❌✅
Генерация тестов ❌✅
Понимание контекста проекта 
ChatGPT в вашем IDE
Рутина вне кода
Рефакторинг ✅❌
Генерация комментариев   ✅❌
Специфичные сценарии ❌✅ ❌❌✅

Мои субъективные впечатления:
1) В коде себя очень неплохо показывают Copilot и Codeium, часто почти на равных. Это радует, учитывая, что Codeium бесплатный.
2) Вне кода же Copilot вне конкуренции, по большому счету ему все равно, что генерировать — код, комментарии, cmakelist, стихи. И это очень важное свойство, надеюсь, AI-плагины в IDE пойдут по этому пути.
3) Tabnine показывает себя аутсайдером, хотя является самым дорогим из всех.
4) FauxPilot можно пользоваться! И это очень круто. Тем не менее большие задержки и необходимость играться с настройками пока делают опыт его использования далеко не идеальным.




Если вам было интересно, и вам хочется делать крутые и удобные инструменты для других разработчиков, то приходите к нам в команду KasperskyOS Tooling Development!


Если же вы хотите погрузиться в недра построения моделей машинного обучения, то обязательно загляните к моим коллегам в команду Data Science & Big Data.


Комментарии (10)


  1. garr1nch4
    08.06.2023 14:42
    +3

    Есть еще Amazon CodeWhisperer. В нем меньше языков, но по субъективным ощущениям с кодом справляется на уровне Copilot


    1. Magn Автор
      08.06.2023 14:42
      +2

      Да, на самом деле претендентов может быть еще больше. Как минимум есть еще replit, tabby ml, StarCoderEx, CodeWhisperer и еще минимум парочку можно найти. Но всех в обзор не впихнешь, брал наиболее популярные на момент составления обзора)


    1. Alex-111
      08.06.2023 14:42
      +1

      По моим субъективным ощущениям заметно слабее, чем Copilot. Бойлерплэт для c++ классов криво генерит. Часто шумит, много мусора подставляет на каждую строку. Не смог с ним совладать.


  1. LordDarklight
    08.06.2023 14:42
    +1

    копилотить для питона ещё куда не шло - хотя без строгой типизации это конечно бред, но и копилотить для C++ - это вообще маразм - уж больно сложный ЯП с кучей зависимостей и супернепрозрачной работой темплейтов - там компилятор в большинстве случаев тупит даже в не очень сложных проекта - а Вы хотите чтобы копилот в этом слёту разобрался!

    Ну не те ЯП для тестирования выбраны совсем не те! Тут бы какой-нибудь управляемый ЯП со строгой статической типизацией, и боле менее единой общей библиотечной базой типов и приёмов работы с ней, подошёл - ну там C#, Kotlin, возможно Rust и TypeScript, в перспективе, возможно, Mojo (как замена для Python)

    Лично мне Copilot хорошо помогает "кодить" в C# - хоть и до идеала ещё очень далеко!

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


    1. LordDarklight
      08.06.2023 14:42

      ну вот и ничем не обоснованные минуса подоспели


  1. aborouhin
    08.06.2023 14:42
    +5

    Copilot меня удивил, когда я решил переписать слишком разросшийся bash-скрипт в виде нормального консольного приложения на C# (понимаю, что C# не лучший выбор для консольных приложений, но чтобы не переключаться, выбрал его). Я только создал проект и начал набирать первые символы - как он тут же предложил мне достаточно корректный "перевод" всего этого скрипта целиком! Единственное, за что он мог зацепиться, чтобы продемонстрировать такой результат, - название проекта соответствовало названию скрипта, а тот скрипт лежал у меня в приватном репозитории на GitHub (не в отдельном, а с ansible-плейбуками, один из которых его деплоил). И он это сделал. В первый момент я просто опешил, пока не понял, откуда он догадался :)


  1. vagon333
    08.06.2023 14:42
    +2

    Благодарю за обзор и примеры использования.
    Как-то не сложилось у меня с Copilot когда его анонсировали в конце 2021 - попробовал и не понял.
    Попробую повторно ...


  1. xserg
    08.06.2023 14:42

    Жаль нет примеров для PHP


  1. Alex-111
    08.06.2023 14:42
    +1

    Крутой обзор, спасибо! Среди тех, что я пробовал, Copilot тоже показался самым полезным.


  1. Kristaller486
    08.06.2023 14:42

    Жаль, что в сравнении нет StarCoder, ведь, судя по всему, это сейчас лучшая открытая модель для генерации кода.