Дмитpий Hecтepук

Блог о программировании — C#, F#, C++, архитектура, и многое другое

Что нового в C# 8

6 комментариев

«Восьмерка» еще даже не вышла RTM а я уже пишу про нее пост. Зачем? Ну, основная идея что тот, кто предупрежден — вооружен. Так что в этом посте будет про то что известно на текущий момент, а если это все посдстава, ну, поделом.

Nullable Reference Types

Я не знаю, в курсе вы или нет, но дизайнеры всех современных языков «продолбали» как минимум несколько важных аспектов. Один из основных продолбов — это неинициализированные объекты, указатель на которые имеет значение NULL в С и nullptr в современном С++. Другой аспект — это zero-terminated strings, когда длина строки вычисляется за О(n). Но мы сейчас говорим за null.

C# это конечно тоже зацепило в расных испостасях, вот например:

public class Person
{
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string MiddleName { get; set; }
  public Person(string first, string last, string middle) =>
    (FirstName, LastName, MiddleName) = (first, last, middle);
  public string FullName =>
    $"{FirstName} {MiddleName[0]} {LastName}";
}

Пример выше очень хорошо иллюстрирует проблему. У человека может быть отчество но у меня, например, в паспорте его нет и следовательно непонятно что писать в это поле. Дефолтное значение строки в C# это null. Если интерполировать это значение в пустую строку ($ за кулисами делает string.Format()), ничего страшного не будет. Но если вы попробуете получить первую букву null-значения, вы получите исключение NRE (NullReferenceException).

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

Системы статического анализа вроде Решарпера уже давно пытаются как-то облегчить участь разработчика, предупреждая о возможных косяках. Собственно для этого придумали аннотации (NuGet пакет JetBrains.Annotations) которые можно добавить в проект вот таким вот образом:

public class Person
{
  public string FirstName { get; set; }
  public string LastName { get; set; }
  [CanBeNull] public string MiddleName { get; set; }
  public Person(string first, string last, [CanBeNull] string middle) =>
    (FirstName, LastName, MiddleName) = (first, last, middle);
  public string FullName =>
    $"{FirstName} {MiddleName[0]} {LastName}";
}

Код выше заставляет системы статического анализа ругаться на возможный NRE в точке MiddleName[0].

Но Microsoft… как всегда берет то, что делает другие и банально копирует. Вообще есть шутка, что в долгосрочной перспективе VS просто скопирует все фичи Решарпера. А потом запретит плагины. Поэтому хорошо что есть райдер.

Короче, МС естественно не стали менять язык. Точнее как не стали, они конечно его поменяли. Вместо решарперных аннотаций, в C#8 можно написать вот так:

#nullable enable

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

var p = new Person("Dmitri", "Nesteruk", null);

вы получите следующий warning:

1>NullableReferenceTypes.cs(26,48,26,52): warning CS8625: Cannot convert null literal to non-nullable reference or unconstrained type parameter.

Да-да, просто предупреждение, а не ошибку (хотя treat errors as warnings никто не отменял).

Ну да ладно, а что дальше? А дальше нам хочется как-то все-таки сказать C#, что теоретически MiddleName таки может быть null поэтому доступ с индексом [0] тоже нужно проверять.

Для этого мы меняем поле на вот такое:

public string? MiddleName;

Хмм, что это? Кому-то может показаться что string? эквивалентно Nullable<T>, но напомню что в этом типе, T : struct, так что очевидно это что-то другое. Этот вопросик — это всего лишь подсказка компилятору, т.к. по факту тип поля все еще обычный string.

Теперь компилятор выдаст вам еще один варнинг на доступ по индексу ноль. И чтобы оно заработало вам придется как-то перестраховаться, например написав:

public string FullName =>  $"{FirstName} {MiddleName?[0]} {LastName}";

Теперь самый важный вопрос: что же поменялось в IL? С точки зрения исполняемого кода — ничего! Но с точки зрения метаданных поменялось конечно: теперь в типе который использует nullable аннотации все типы которые являются nullable проаннотированы атрибутом [Nullable]. Сделано это по понятным причинам: чтобы потребители вашего кода могли использовать ваши аннотации.

Чувствительность к проверкам

Аннотации в C#8 работают в какой-то мере как Котлиновские смарт-касты. Иначе говоря, если у меня есть апи который выдает nullable-тип

string? s = GetString();

То конечно при попытке достучаться до первой буквы я получу warning. Но если я напишу так:

if (s != null){  char c = s[0];}

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

Предотвращение лишних проверок

Есть два способа отключить проверки на null. Первый — это просто писать код без «вопросиков», тем самым констатировав тот факт что все твои поля, параметры и так далее вообще ни при каких условиях null-ами быть не могут.

Второй подход — это явно сказать компилятору что в этой точке проверка не нужна. Вот несколько примеров:

  • (null as Person).FullName конечно выдаст нам warning

  • (null as Person)!.FullName warning уже не выдаст, т.к. мы явно просим не проверять выражение

  • (null as Person)!!!!!!!!!!!!!.FullName тоже является валидным выражением и тоже отключает проверки

  • (null as Person)!?.FullName тоже валидно и все же делает в этом случае проверку на null; примечательно что обратное использование, ?!, не скомпилируется

Проверки в либах

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

Type t = Type.GetType("abracadabra");Console.WriteLine(t.Name);

и не получить никакого предупреждения. Я-то знаю что Type.GetType() возвращает null когда даешь невалидное название типа, но поскольку BCL пока еще не размечена nullable аннотациями, компилятор это съедает.

И нет, мы не можем «форсировать» подобные проверки кодом вроде

Type t = Type.GetType("abracadabra");
Type? u = t;
Console.WriteLine(u.Name);

Код выше все равно не выдаст предупреждение. Очевидно, компилятор считает что t != null, следовательно u тоже не может быть равно null, сколько его не декорируй.

Итого

Nullable reference types — сомнительной полезности фича, которую еще рано использовать. Оригинальности в ней мало. В долгосрочной перспективе она, конечно, должна помочь нам как-то бороться с рисками nullability но глобально она проблему, как вы понимаете, не решает.

Индексы и Диапазоны

На матстатистке есть такое хороше упражнение: брать диапазон чисел, обладающих тем или иным распределением, преобразовывать его, и потом считать статистику по результату. Например, если X~N(0,1), чему равны E[X²] и V[X²]?

Диапазоны это очень хорошо, но дизайнерами сишарпа явно хотелось получить диапазоны в стиле Питона, а для этого пришлось ввести понятие «с конца».

Итак, у нас за кулисами появляются два новых типа: Index и Range.

Index

Вы никогда не задумывались, почему это индекс в массив обязательно int а не uint? Все просто: этот ваш пресловутый индекс это просто отступ от указателя на начало массива (привет Си). Поэтому он теоретически может быть отрицательным, хотя конечно в C# запись x[-1] лишена всякого смысла.

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

В C# пошли другим путем и ввели новый синтаксис. Но, все по порядку. Для начала, ввели новый тип под названием Index который представляет индекс элемента в массиве, строке или вашей собственной коллекции:

Index i0 = 2; // implicit conversion

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

У самого типа Index есть два свойства (зачем сделали свойства а не поля — тот еще вопрос):

  • Value, то есть сколько элементов нужно отсчитать

  • IsFromEnd — булевое значение, показывающее, нужно ли отсчитывать от конца коллекции а не от начала

Структуру можно инициализировать просто вызвав конструктор:

Index i1 = new Index(0, false);

Код выше, как вы поняли, берет первый элемент сначала. А вот последний элемент (то есть нулевой, но с конца) можно взять вот так:

var i2 = ^0; // Index(0, true)

Опаньки! Многие из вас наверное хотели, чтобы оператор ^ сделали возведением в степень (мне как математику эта идея комфортнее, LaTeX, все такое), но по факту это теперь специальный синтаксис для создания индекса «с конца».

Встроенные типы, такие как массивы или строки, конечно же поддерживают индексер (operator this[]) для типа Index, так что смело можно писать

var items = new[] { 1, 2, 3, 4, 5 };
items[^2] = 33; // 1, 2, 33, 4, 5

Range

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

X..Y

Что означает «все элементы от X включительно до Y». При этом, включает ли диапазон значение с индексом Y или нет — нельзя сказать не посмотрев на Y. Об этом очень скоро.

Итак, вот несколько примеров:

  • var a = i1..i2; // Range(i1, i2) — полноценный диапазон с началом и концом

  • var b = i1..; // Range(i1, new Index(0, true)); — диапазон от i1 и до конечного элемента

  • var c = ..i2; // Range(new Index(0, false), i2) — диапазон от самого первого элемента и до индекса i2

  • var e = ..; — вообще весь диапазон, то есть от первого и до последнего элемента

  • Range.ToEnd(2); — эта и подобные статический функции — как раз то, что использует компилятор за кулисами; все это можно лицезреть, если открыть сборку в dotPeek, ilSpy или другом декомпиляторе

Включается ли конечный элемент?

Окей, знаете что в С++ толстым слоем разборсано неопределенное поведение (undefined behavior?). Ну так вот, в реализации Range в C# тоже затаился сюрприз. Суть примерно в следующем: последний элемент включается только если он взят «с конца».

Представьте массив x = {1, 2, 3}. Если взять x[0..2] вы получите {1, 2}, а вот если взять x[..2] или например x[..], вы получите {1, 2, 3}.

Это немного выносит мозг т.к. элементы x[2] и x[^1] — это одинаковые элементы, но семантика их поведения как часть Range-а разная!

Еще немного семантики

Во-первых, диапазон X..Y будет работать только если X <= Y (надеюсь вы включили лигатуры?). Если же вы решили сделать нисходящий диапазон (например 7..3), то вы просто получите ArgumentOutOfRangeException. Это очень неприятно т.к. часто хочется получить индексы массивы как некоторые выходные значения из функций, и проверять что диапазон восходящий ну совсем не хочется.

Во-вторых, «шаг» не включен в спеку, то есть нельзя написать 1..2..100 и получить только нечетные числа. А было бы удобно. Эта фича реализована в языках вроде MATLAB.

Range ведет себя в стандартных типах вот так:

  • В массивах, он дает копию подмассива, прям копируя каждый элемент

  • В строках происходит вызов Substring(). Строки в C# иммутабельные так что создается новая строка.

  • На коллекциях можно вызывать AsSpan() передавая ему Range.

  • В Span тоже можно засунуть RangeSpan.Slice(), получив под-диапазон.

Конечно, вы можете также запилить поддержку Index/Range в своих собственных типах с помощью переопределения operator this[]. При этом нужно понимать, что не во всех коллекциях понятие «с конца» работает хорошо. Например, если у вас односвязный список (singly linked list), то брать что-то «с конца» — плохая идея.

Итого

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

И да, решарпероводам будет много всяких веселых инспекций:

Default Interface Members

Никто не рискует породить столько ненависти сколько реализация дефолтных интерфейс мемберов, то есть бархатное преврашение интрефейсов в абстрактные классы. И первый вопрос, на который нужно ответить — зачем?

Ну типичная мотивация такая. Вот допустим вы хотите сделать Enumerable.Count() — его конечно можно делать через while (x.MoveNext()) но это немного бредово для коллекций, чья длина известна заранее. Поэтому мы втыкаем в Count() проверки типа:

if (x is IList<T> list)
  return list.Count;

Логично? А как насчет IReadOnlyList<T>, для него такая оптимизация будет? Что значит «не сделали»?!?

Ситуация, приведенная выше наводит нас на вынужденное, очень грубое нарушение open-closed principle да и других принципов SOLID, т.к. делать проверки на все возможные типы ради оптимизация — это провальная затея.

А что можно сделать? Ну, можно было бы, чисто теоретически, как-то взять и добавить реализацию Count(), специфичную для IReadOnlyList<T>, прямо в интерфейс, возможно как метод расширения. Только это тоже не сработает! Откуда мы знаем что экстеншн-метод IReadOnlyList<T>.Count() является перегрузкой метода IEnumerable<T>.Count()? Правильно, не знаем! Нужен другой подход.

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

Тонкости использования

Но давайте для начала посмотрим на более приземленный пример:

public interface IHuman
{
  string Name { get; set; }
    
  public void SayHello() 
  {
    Console.WriteLine($"Hello, I am {Name}");
  }
}public class Human : IHuman
{
  public string Name { get;set; }
}

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

Human human = new Human() { Name = "John" };
human.SayHello(); // will not compile

Странно да? Вроде бы, валидный код. На самом деле нет — дело в том, что конкретный класс, хоть он и реализует тот или иной интерфейс, понятия не имеет о дефолтных методах этого интерфейса.

Почему, спросите вы? Потому, что вся соль этих методов в том, чтобы добавлять их пост фактум, когда вашим интерфейсом уже пользуются. А что если за это время класс Human обзавелся собственным SayHello()? Правильно, будет конфликт.

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

IHuman human = new Human() { Name = "John" };
human.SayHello();
((IHuman)new Human { … }).SayHello();

Наследование интерфейсов

Все, что я описал выше, наводит нас на интересную мысль: если два интерфейса реализуют одинаковый метод Foo() (с деволтной реализацией), класс может реализовать оба этих интерфейса и получить две независимые реализации Foo(), обе из которых можно вызывать:

public interface IHuman
{
  string Name { get; set; }
  
  void SayHello() 
  {
    Console.WriteLine($"Hello, I am {Name}");
  }
}
public interface IFriendlyHuman : IHuman
{
  void SayHello()
  {
    Console.WriteLine(      $"Greeting, my name is {Name}");
  }
}
((IHuman)new Human()).SayHello();
// Hello, I am John
((IFriendlyHuman)new Human()).SayHello();
// Greeting, my name is John

Заметьте, что в коде выше IFriendlyHuman.SayHello() как бы должен override-ить IHuman.SayHello(), но этого не происходит! Что же нужно сделать чтобы вызов SayHello() стал действительно виртуальным? Нужно явно сказать об этом:

public interface IFriendlyHuman : IHuman
{
  void IHuman.SayHello()
  //   ↑↑↑↑↑↑
  {
    Console.WriteLine(      $"Greeting, my name is {Name}");
  }
}

Вот в этом случае вызов SayHello() на любом интерфейсе, будь то IHuman или IFriendlyHuman будет виртуальным и уже не важно, к какому из них вы скастовались:

((IHuman)new Human()).SayHello();
Greeting, my name is John
((IFriendlyHuman)new Human()).SayHello();
Greeting, my name is John

Diamond Inheritance

Естественно, в ситуации когда вы можете иметь два «честных override-а» в двух интерфейсах-наследниках породит конфликт в случае, если вы попытаетесь реализовать их оба:

interface ITalk { void Greet(); }
interface IAmBritish : ITalk
{
  void ITalk.Greet() => WriteLine("Good day!");
}
interface IAmAmerican : ITalk
{
  void ITalk.Greet() => WriteLine("Howdy!");
}
class DualNational : IAmBritish, IAmAmerican {}
// Error CS8705 Interface member 'ITalk.Greet()' does not have a most specific implementation. Neither 'IAmBritish.ITalk.Greet()', nor 'IAmAmerican.ITalk.Greet()' are most specific.

Проблема тут в том, что компилятор не может найти «более специфичный» (то есть, ниже в иерархии наследования) интерфейс для использования и, в результате равнозначности, не поймет что нужно делать если кто-то вызовет какой-нибудь IAmAmerican.Greet() — ведь по идее нужно гулять по виртуальной таблице, а куда идти-то, если варианта два?

Итого

Фича для писателей АПИ. Обычным пользователям скорее всего не стоит беспокоиться, особенно если вы, как я, контролируете весь свой код и вам не страшно в любой момент менять его API. Единственный реальный кейс — это когда вот прям нужно оверрайдить экстеншн-методы. У вас есть подобные юз-кейсы?

Pattern Matching

Эту фичу нагло крадут из F# уже на протяжении нескольких минорных релизов. Конечно — в F# ведь эта фича очень удобна, но она идет рука об руку с теми фичами, которых в С# нет, а именно алгебраические типы и функциональные списки.

Но несмотря на это, аналогом F#ного match стал С#ный switch. В C#8 все это обрастает дополнительными возможностями.

Property Matching

Если у объекта есть поля или свойства, можно мэтчить по ним:

struct PhoneNumber{
  public int Code, Number;
}
var phoneNumber = new PhoneNumber();
var origin = phoneNumber switch {
  { Number: 112 } => "Emergency",
  { Code: 44 } => "UK"
};

Код выше анализирует структуру объекта phoneNumber, проверяя его поля на конкретные значения. Заметьте что у нас не switch statement а switch expression, то есть выражение которое делает возврат лямбда-образным синтаксисом.

Несмотря на всю лаконичность, код выше — бажный, так как не отлавливает все кейсы. У дизайнеров было два выбора: либо молча сглатывать неохват паттерна делая default-init (иначе говоря, возвращать default(T) из свича у которого не замэтчился ни один паттерн) или же бросать исключение. Микрософт выбрали второе, так что если запустить код выше, мы просто получим:

Ну и системы статического анализа кода тоже в долгу не останутся:

Загадка

Поскольку мы можем между фигурных скобок анализировать все что угодно, мы можем сделать и кейс, где фигурные скобки пустые:

var origin = phoneNumber switch {
  { Number: 112 } => "Emergency",
  { Code: 44 } => "UK",
  { } => "Indeterminate",
  _ => "Missing"
};

Что это значит? Да то, что теперь есть 2 кейса: _ (подчеркивание), которое словит абсолютно любой результат, и {} которое отловит любой аргумент который не null.

Пример выше определенно покрывает все кейсы, так что исключение мы на нем не словим. А что насчет вот такого?

var origin = phoneNumber switch {
  { Number: 112 } => "Emergency",
  { Code: 44 } => "UK",
  { } => "Unknown"
};

Покрывает ли пример выше все варианты? Если phoneNumber это struct — то конечно да, но представим что это class. С одной стороны кто-то может сказать что да, не покрыли ситуацию с null, но… ведь есть nullable reference types!

Вот и получается, что покрытие всех вариантов зависит не только от типа объекта но еще и от контекста компилятора.

Рекурсивные паттерны

Все-таки с неймингом у МС получается ооочень плохо, прям фееричный булшит. Сначала nullable reference types — масло масляное, ибо референсные типы являются nullable по определению, но термин recursive patterns — это еще на порядок хуже.

В F#, поскольку там есть функциональные списки, можно рекурсивно проверять подсписки списка на соответствие паттернам. В C# же, этот термин значит совсем другое, причем как я уже сказал, нейминг этой фичи в C# — просто шлак.

Если коротко, «рекурсивные паттерны» в C# это возможность углубиться внутри структуры того или иного объекта и проверить еще и поля-полей, если так можно выразиться:

var personsOrigin = person switch {
  { Name: "Dmitri" } => "Russia",
  { PhoneNumber: { Code: 46 } } => "Sweden",
  { Name: var name } => $"No idea where {name} lives"
};

В примере выше проиллюстрированы сразу две идеи. Первая — это то, что можно углубиться в объект person.PhoneNumber и промэтчить его поле Code на значение 46. Вторая — это то что вместо мэтчинга можно задекларировать переменную и в нее записать найденное значение чтобы им с последствии пользоваться. Это тоже иногда бывает удобно, и конечно же работает в контексте этой надуманной «рекурсивности».

Валидация

Хорошее применение всей этой кухне: комплесная валидация разных аспектов одного сложного объекта в одном switch-е. Вот например:

var error = person switch {
  null => "Object missing",
  { PhoneNumber: null } => "Phone number missing entirely",
  { PhoneNumber: { Number: 0 } } => "Actual number missing",
  { PhoneNumber: { Code: var code } } when code < 0 => "WTF?",
  { } => null // no error
};
if (error != null)
  throw new ArgumentException(error);

Как видите, в коде выше идет несколько проверок: простые проверки с паттернами, плюс ключевое слово when для сложных сценариев когда иден не сравнение с одним значением, а нечто более хитрое.

Интеграция с проверками на типы

Проперти-паттерны можно «поженить» с проверками на типы которые появились одним сишарпом ранее. Теперь можно проверить на тип, а потом еще и распаковать структуру:

IEnumerable<int> GetMainOfficeNumbers()
{
  foreach (var pn in numbers)
  {
    if (pn is ExtendedPhoneNumber { Office: "main" })
      yield return pn.Number;
  }
}

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

Деконструкция

Надеюсь вы не забыли про деконструкцию — фичу языка которая позволяет, по сути, распаковывати тип в кортеж. Для этого тип должен реализовывать метод Deconstruct() который просто выгружает содержимое типа в out-параметры.

Так вот, эту фичу тоже можно поженить с паттерн-мэтчингом и получить следующее:

var type = shape switch
{
  Rectangle((0, 0), 0, 0) => "Point at origin",
  Circle((0, 0), _) => "Circle at origin",
  Rectangle(_, var w, var h) when w == h => "Square",
  Rectangle((var x, var y), var w, var h) =>
    $"A {w}×{h} rectangle at ({x},{y})",
  _ => "something else"
};

Код выше распаковывает содержимое прямоугольника или круга в кортежные структуры, при этом есть варианты: либо заниматься паттерн-мэтчингом, либо просто деструктурировать объекты в переменные, как показано в последнем примере. Заметьте что этот процесс тоже является «рекурсивным» — у прямоугольника есть точка начала, которая деструктурируется в круглых скобках. Прочерк (_) используется для тех аспектов деструктуризации, которые нам не интересны.

Итого

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

Заключение

Что, я не все фичи показал? Ну, я и не обещал, вообщем-то. Для начала хватит. Пока язык не релизнули я могу еще чуток поисследовать. А если и вы хотите поисследовать, вам потребуется VS 2019 Preview (да-да, preview версия а не RTM), .NET Core 3 (многое из описанного выше попросту не поддерживается в .NET Framework), ну и dotPeek тоже будет полезен чтобы понять, что же там за кулисами.

У меня пока все. Продолжение (возможно) следует. ■

Реклама

Written by Dmitri

1 июня 2019 at 21:16

Опубликовано в C#

Tagged with

Мои мысли насчет IDE и прочих developer tools

10 комментариев

Поскольку я 5+ лет провел в JB, конторе которая пилит IDE, у меня накопилось много опыта и мнений насчет того как эти «ИДЕ» стоит строить. Сразу скажу, что непосредственно в разработке IDE я практически не участвовал — как я уже говорил, в JB я попал в то странное время в моей жизни, когда на «карьеру программиста» мне было чуть более чем совсем, так что становиться кодером и слить 10+ лет жизни за весьма-весьма скромные деньги я не планировал.

Тем не менее, у JB в плане сред разработки есть хорошая, отработанная, модель. Нужно понимать что JB ультраконсервативен — подход всегда один, он идет под копирочку: если есть язык с неким спросом, то на базе IDEA делается IDE которая реализует все то же, что реализовали другие IDE. Кумулятивный опыт дает это сделать быстро и эффективно, а фишечки вроде Котлина дают это сделать еще и приятно (т.к. Java — это фифифи, никто не хочет об нее руки пачкать).

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

Аппаратно ускоренный редактор

Знаете, мы вот в алготрейдинге не пишем терминалы для обычной виндовой консоли. И оконных приложений не пишем. Почему? Да потому что скорость, что у обычной текстовой консоли (cmd.exe итп) что у оконных приложений — просто шлак. Они тратят больше времени на отрисовку чем на обработку потоков данных. Да, это конечно няшно что виндовая консоль потокобезопасна, но я такое же запилил на коленке, с множественными viewport-ами и буферами. Я на этом собаку съел.

А как надо, спросите вы? Да все просто. Рисуем текстурку на которой ровно 256 буков, ибо использовать юникод не нужно, а языки вроде русского (прости хосподи MOEX) вполне себе в эти 256 влезают. Так вот, на тестурке рисуем каждую из буков, а потом из этой текстурки рендерим каждую букву как прямоугольник на 2D полотне, со всем прекрасным аппаратным ускорением которое нам дают дорогущие видеокарты.

Зачем все это? Да потому что нужна скорость. И она нужна везде. Это бред когда ты редактируешь текст в ИДЕ и у тебя suka залип текстовый редактор! Это просто тотальный бред! Я понимаю что залипания там по вине всяких комплишнов, и что все IDE постепенно прут к zero latency, но я хочу чтобы для рисования одной отдельно взятой буквы не возбуждались, например, подсистемы интерпретации содержания шрифтов. Какая-нть GDI которая делает DrawText() сливает перформанс настолько, что это даже не смешно, это все тотальный позор.

Кто-то скажет «а как же юникод?» — да в гробу я его видал. Если вы вдруг определили символ дерева через kanji или засунули в код смайлик, мне вас не жалко вообще, используйте Visual Studio или IDEA или что у вас там. И продолжайте лелеять мысль что у вас там «аппаратное ускорение». Ага, щас.

На самом деле, плюсов от текстурно-ориентрированного текст редактора миллион — это в том числе и идеальная плавность скроллинга, возможность очень плавного движения курсора и так далее. Кстати, в моих последних видеокурсах это все представлено как некий рендер — можете посмотреть там превьюшки, и это не предел фантазии!

Короче о чем я — редактирование нужно упрощать и делать аппаратным полностью. Реально много крутого можно сделать, но самое главное это то, что скорость отрисовки феноменальная!

Языковые и source-сервисы

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

Это часть задачи. Другая часть — это сделать весь этот процесс распределенным. Идея о том что только один комп анализирует код, например, или компилирует, или тестирует — это достаточно бредовая идея. Нужная полная распределенность во всем. У нас уже есть примеры вроде IncrediBuild который раскидывает компиляцию по сети, то же самое нужно делать и с анализом и прочими вещами.

Выбор языков программирования

То, что JB сделали все на джаве и сишарпе — это одновременно огромный плюс и огромный косяк. Фразы про «решарпер тормозит» я слышал 5 лет подряд. Если бы в то время не изобрели SSD, вся эта кухня вообще застряла бы в оптимизациях, т.к. на обычных жестких дисках работать со всей этой хитрой инфраструктурой невозможно вообще. Над перформансом там работают постоянно и это, с одной стороны, хорошо, но с другой стороны отчасти виноваты те платформы которые были выбраны для всего этого — JVM и .NET.

В high-performance среде очень важен детерминизм. Вы напарсили 10000 файлов и наплодили временных объектов — кто их будет удалять, и когда? По личному опыту я знаю, что когда нужен перформанс, ты начинаешь жестко копаться во всяких ref struct, пинить массивы и еще 100500 непотребных вещей.

Да, С++ тяжелый язык и в нем много косяков. Но на нем блин самолеты летают и танкеры плавают! Да, бывают проблемы, язык не совершенен, предостаточно странных решений и немного муторный процесс стандартизации.

Но С++ дает тебе «честные» деструкторы. Он дает как ручной контроль памятью (иногда бывает нужно), так и возможность использовать «умные» указатели. Это намного более предстказуемо чем взять и нааллоцировать тысячу мелких объектов и потом жаловаться что GC делаеть stop-the-world как раз в тот момент когда пользователь пытается вызвать какойнть компшишн.

Еще одна «фишечка» в том, что уйдя на управляемые платформы, мы сразу теряем доступ к таким вещам как микрооптимизации. Такие вещи как использование SIMD или Assembler-ных вставок позволяют реализовывать некоторые вещи не в десятки, а в сотни раз быстрее! Сколько бы не говорили производители этих рантаймов, что JIT заоптимизирует ваши циклы, это все наглая ложь!!! Автоматизация этого происходит только на самых тривиальных, «детских» циклах где все очевидно. Когда у вас появляется хотя бы малейшая зависимость, ваша параллельная или SIMD-овая карета превращается в тыкву.

Особняком стоит ситуация со строками, что какбэ актуально, т.к. в .NET строка — это немутабельная UTF16 конструкция. Это совершенно непрактично по ряду причин. Во-первых, инвариант sizeof(char) == 1 дает возможность детерминированно обходить ASCII-буквы без извратов. То есть, вы прикинули ситуацию? У вас в 99.99999999% случаев только ASCII в коде, но вы платите огромную таксу за то что System.String реализует весь свой API c допуском что там может быть всякие юникодные corner-cases.

По секрету скажу, что в дотнете у меня для алготрейдинга просто написан тип Str — это как string но он строго ASCII, просто держит массив байтов. Естественно, всякие Contains() становятся вообще тривиальной задачей.

Что еще хорошего от однобайтовых ASCII строк? Да то что они очень хорошо ложатся на GPU. И если вы думаете что GPU тут неприменим, вы просто не научились его готовить — если держать список строк в прямоугольном массиве памяти (представьте себе буфер редактора, ограниченный по горизонтали), то внезапно можно весь этот кусок памяти через P/Invoke скормить CUDA, которая на нем сможет делать такие банальный, казалось бы, вещи как Search & Replace. Это работает не просто «хорошо», это работает офигенно. Особенно на крупных проектах, с которыми, как вы знаете, у всех IDE огромные проблемы, которые не лечатся закупкой RAM-а.

Я могу еще долить про строки если хотите. Помимо юникода, есть еще такая проблема как немутабельность. В частности, эта проблема заставляет многих разработчиков реализовывать свой Substring(), т.к. фабричный порождает новые строки, а если вам просто хочется некий эфемерный StringRange, вам придется приготовить его самим, со всеми операторами, конверсиями, набором методов и прочей кухней. Заметьте — вы можете передать ref string в метод, но вы не можете записать адрес строки в поле. Можно долго извращаться со всем этим, но потом ты понимаешь, что даже со всем запредельным бредом в АПИ, std::string не так уж и ужасен.

Вообще, в большинстве случаев, мы все равно пишем свой str (печатать string слишком долго). Суть заключается в том, чтобы сделать нужные операции как можно дешевыми. В качестве «кустарного» примера можно посмотреть на folly/fbstring.

Аппаратное ускорение

Если мы с вами договорились, что редактор и сорцы/анализ должны находиться в разных местах, то кто мешает на бэк-энде пошаманить еще в направлении анализа кода и всего вот этого? И я сейчас не столько про всякие Optane-ы (хотя хорошее железо тоже нужно), а скорее про специализированные решения в области анализа.

Например, почему бы не загрузить весь working set разработчиков вообще в оперативку? Ну, подумаешь съест там сколько-то гигов на разработчика, всяко анализ пойдет легче пока вам не нужно копать файловую систему со всеми ее ACLами и прочими прелестями. Это кстати и компиляцию тоже ускорит существенно, если что — я например до сих пор, несмотря на то что 2019й на дворе, компилируют крупные вещи вроде Boost на RAM-диске. Ибо нефиг.

Еще одна фишечка это GPGPU, про которые я уже упомянул. Более того, можно не только держать прямоугольные массивы строк, можно также держать базы данных прямо на GPU. Это свежая, развивающаяся область, и там очень много интересного, в т.ч. уже сущестуют коммерческие решения. Представьте себе базу данных, которая ищется в десятки/сотни раз быстрее чем даже если бы она была в RAM полностью.

Касательно FPGA — тут тоже много идей. Одна из них в том, что порой на FPGA очень удобно реализовывать структуры данных. Особенно если это не гигантские но performance-critical структуры. Вообще про FPGA должен наверное быть отдельный разговор т.к. это «темная лошадка» всей этой схемы. Я-то конечно научился уже работать с FPGA-картами через шину PCI (нет ничего сложного если у тебя есть правильный софт), но как получить от них максимальный прирост именно с точки зрения анализа и других девелоперский фич — это вопрос. Одна из идей которые достаточно хорошо раскопаны — это лексеры, т.к. лексический анализ это обычно data-intensive проблема которая очень хорошо ложится на эту архитектуру. Другое дело что лексинг — это только часть общего процесса.

Спекулятивные вычисления и машинное обучение

Как показывает мой проект CallSharp, вполне реально «выводить» подходящий код по набору входных-выходных значений. Я кстати планирую эту тулу допилить до нужного состояния и возможно даже продавать ее, т.к. эта штука реально открыла мне глаза на возможности computer-assisted coding, даже в условиях когда ты делаешь банальный перебор.

Машинное обучение конечно должно идти дальше. Например, я считаю что делать тесты для методов руками — это нонсенс. Если есть некий функционал, мы как минимум должны иметь возможность генерировать для него test scaffolding который потом можно заполнять тестовыми данными. This is low-hanging fruit.

Тут очень сложно предсказать, по какому пути все пойдет. Я например огромный фанат суперкомпиляции — подходу, в котором компьютер выводит по нужным данным цепочку последовательностей комманд. Сейчас это делают, в основном, на микро уровне, т.е. на уровне инструкций процессора. Порой результаты просто ошеломляют — ведь в отличии от человека, компьютер будет напрямую использовать всякий bit twiddling (сдвиги итд.) для того чтобы решить задачу эффективнее.

В идеале, хотелось бы достичь такого состояния, когда компьютер не лексит/парсит на основе описания грамматик, а сам начинает изучать поведение компилятора и на основе этого строит наиболее эффективные структуры для разбора и анализа кода. Вот это то, на что я бы очень хотел посмотреть, но к сожалению «а воз и ныне там» и все видимо занимаются другими, более важными делами.

Заключение

Я не исключаю что мне самому захочется позаниматься чем-то связанным с IDE но если я и буду это делать, то абсолютно все каноны современного ИДЕстроительства будут выкинуты в помойку, т.к., в ретроспективе, понимаешь какое количество родовых травм есть у всех этих систем.

Если коротко, то основные фичи — это

  • Аппаратный рендер текста (плавный скроллинг, анимации итд)

  • Разнесенные на отдельные процессы/компы редактор, компиляторы, анализаторы и все такое

  • Выборочное аппаратное ускорение различных девелопер-специфичных процессов

  • Спекулятивные вычисления — непрерывная компиляция, тестирование и все в этом духе

  • Машинное обучение которое помогает кодить и диагностировать такие ошибки, которые обычные тулы диагностировать не могут

На самом деле, самое интересное — это то о чем я не написал, то есть такие парадигмы работы с кодом, которые мы сами и не знаем. У меня есть некоторые… домыслы, например, что ИДЕ должна вывешивать наружу нечто вроде терминала который обрабатывает комманды типа «добавь класс». Думаете бред? А ничего что любой Autocad уже реализует подобное? Почему такое скриптование есть во всяких 3Д системах но его нет в наших ИДЕ — ума не приложу. Надо сделать.

JB сделали три русских парня со стартовым бюджетом в жалкие $300k. Блин, да я $300k хоть сейчас положу на стол если будет полноценное видение всей этой кухни, понимание как ее реализовывать и талантливые кодеры, которые могут все это заимплементить. Ведь мне кажется что все это поле developer tools точно готово к инновациям и disruption.

Короче надо думать. ■

Written by Dmitri

27 апреля 2019 at 1:55

Опубликовано в Development

Воспоминания про универ

11 комментариев

Я не фанат высшего образования. Я вообще не фанат организованного образования, т.к. в упор не понимаю, почему это вообще нужно организовывать в какую-то систему. В этом посте я постараюсь описать мой опыт в универе и прочих местах. Но в основном в универе.

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

В то время, я считал что если ты уже не кодер когда тебе 18, то тебе поздно. Сейчас, смотря на современных детей, я понимаю что планка съехала вниз… дети сейчас программируют с начальных классов, и годам к 10 «написать программу» это для них нормальное явление, хотя конечно они еще не вкуривают программы так же системно, как мы. Но всякие Lego MindStorms и прочие плотно захавали из мозг, а С/С++ и Arduino на курсах робототехники тоже практически стали нормой.

Так вот, к 18 годам я попал в один из «топовых» универов Англии который, впрочем, специализируется в основном на электронике а не на comp sci. И я не сказать чтобы впечатлился тем, что я увидел, хотя свою университетскую программу на то время (2000-2003 год) я запомнил описательно.

В плане программирования у нас была Java и С++. Часть преподавалась как теория, часть как практика в лабораториях (которыми руководили всякие postgrads), а для особо продвинутых студентов (таких как я) были доролнительные занятия под названием Space Cadets где можно было написать что-то более сложное.

Если честно, меня не впечатлило ничего. Было очевидно, что преподавателям было глубоко пофиг на эту джаву, они витали в облаках своих абстрактрых hypermedia и язык знали ровно на том уровне, который им позволял что-то преподавать. При этом, те темы которыми занимались преподы были унылы, академичны и к реальной жизни не относились от слова совсем. Например, все пели песню про semantic web, двунаправленные гиперссылки и кучу всяких других абстракций которые индустрия никогда бы не вредила. Пожалуй это первый раз в жизни когда я почувствовал пропасть между академией и реальной индустрией которая делала что-то полезное.

Практически сразу же у нас начались такие бессмысленные предметы как «формальные методы» — это когда используешь языки формального описания программ которые позволяют, например, доказать их корректрость. Хорошо в теории, но если ты не разработчик NASA или Boeing, тебе такие навыки нафиг не нужны. Особенно при том что сами преподы не могли банально объяснить, как язык Z или B ложился на реальный код. Все это было выкинутым временем и можно было сжать до одной 45-минутной лекции. Хотя исследований на эту тему в академии тоже было уух. В этом предмете я впервые увидел препода, который заваливает тебя десятками статей. Это правда было факультативно, а то бы я повесился.

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

Еще один предмет что у нас был это data structures and algorithms, который почему-то поместили в один семестр. Там были, в принципе, годные идеи о сложности и том какие структуры и когда использовать, но в то время меня интересовало только одно: как получить результат малой кровью. Напомню, что в то время Java была в зачаточном состоянии, структуры были нетипизированные (они и сейчас по факту нетипизированные, несмотря на дженерики) и все это выглядело немного адово.

У нас была дискретная математика и обычная математика (на достаточно базовом уровне — не «вышка» как в универах в РФ). Это как раз то что у меня получалось лучше всего, как ни странно.

У нас было 2 предмета связанных так или иначе с «логическим» программированием. В одном мы изучали Scheme (этакий LISP), в другом — Prolog. Оба эти языка мы прошли очень поверхностно, и я помню что преподам было очень неприятно когда я приставал к ним со всякими распросами. Мне кажется им самим было глубоко параллельно на все это.

В плане собственно software engineering у нас было несколько предметов. Был предмет Professional Issues который обсуждал всякие soft skills и всякую хрень которая происходит в индустрии, как нужно делать презентации, копирайт и прочие этические вопросы. Мимо меня это прошло все чуть более чем полностью.

Кстати, часть всей этой кухни была игра под названием software management game — достаточно безумное времяпрепровождение для студентов, этакий fantasy league, где нужно выбирать на что твоя компания потратит деньги каждую неделю чтобы завершить проект. Пользы от такого симулятора — почти нуль, но хоть был какой-то контекст чтобы пообсуждать как это все менеджится. Победителям, если я правильно помню, выдали кнут (!) в рамочке.

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

Все конечно встрепенулись и начали вкладывать хоть какую-то лепту в общий проект. При этом, пока все работали в notepad, у меня был JBuilder и эффективность моя была все-таки повыше. Также, в этом проекте у меня создался первый в моей жизни профессиональный конфликт. Интересно? Ок, рассказываю.

Из 5 человек в нашем проекте была одна девушка. Она занималась тем, что на каком-то сайтике своем рисовала каких-то смешных анимированных персонажей. Вот собственно и все. Были ли у нее скиллы собственно программирования или нет, никто не узнает, т.к. ее вклад непосредственно в сорцы был минимален.

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

Девушка эта очень обидилась. Она посчитала что я как бы нивелировал ее вклад в проект. Я спросил коллег по проекту, кто прав, но они политкорректно пожали плечами и посоветовали мне чтобы я «не парился».

Ну, как-то вся эта история замялась, но потом эта девушка напечатала отчет, который на самом деле должен был делать тот кто «вел» нашу группу (очередной graduate student), но она его написала для него и послала всем нам чтобы проверить. И я написал ей «честный» фидбэк про то где у нее какие ляпы, приправив мои комментарии изрядной долей D&D-юмора про unholy destruction upon us all и все в этом духе.

Вот тут девушка на меня обиделась уже не на шутку. Нужно понимать, в то время еще не было никаких oppression olympics, поэтому девушка по имени Донна просто написала мне грозное письмо с недовольствами и перестала являться на групповые втречи. Было очевидно, что критика ее сильно задела и она восприняла ее как нападение на ее личность. Вообщем, она рано или поздно вернулась в реальность, но все это было очень неловко, а я, как и мои друзья, не мог понять что это она с цепи сорвалась. Забегая вперед, скажу что все мои попытки работать с женщинами над совместными проектами провалились именно из-за излишней эмоциональности оных. Хочется больше профессионализма!

Отдельно хочется упомянуть мой индивидуальный проект. С 3го курса, как я уже говорил, я попал к психологам — атмосфера на психологии мне понравилась намного больше чем на comp sci, поэтому мой дипломный проект я сделал там, написав систему для проведения онлайн исследований в области психологии (система кстати проработала лет этак 10!).

Так вот, мой основной фейл был такой: в то время, вместо java applets, я использовал плагины Macromedia Authorware, ныне умершей технологии. И не подумал про то, что этот плагин не будет работать в стенах моего собственного факультета. Вообще нигде. Часть людей использовали Linux (а плагин был Windows-only), а на студенческих компах банально не было прав чтобы его запускать в браузере! Так что когда я пошел «защищаться», все пошло не очень хорошо. Я не помню какая у меня была оценка, но невысокая.

Конечно, за 3 года было много «элективов» разной степени булшитистости. Был французский язык (нужно было иметь хоть 1 гуманитарный предмет), странные предметы вроде Artificial Intelligence (прогулял все лекции, готовился за ночь, получил >50%), а также предметы которые я просто забыл.

Какой вывод — да простой вывод из всей этой истории. Универ — это какой-то потрясный способ сжечь 3 года жизни и денег и при этом получить минимом скиллов. Программировать меня не научили… я даже Java забросил на 3 курсе т.к. вышел C# а я всегда хотел работать только на Windows поэтому мне было пофиг на всю эту кроссплатформенность и иже с ним. А на С++ в универе никто не умел программировать тогда и не умеет сейчас. Единственное исключение — это русские! Поэтому они сейчас и рулят там HPC делами.

Будь я сейчас студентом, я бы пошел на математику. Это и сложнее, и более фундаментально, и не устревает, применимо много где. А программирование — это не наука. Это навык. Чтобы программы писать. Ведь ключевым вещам вроде отдадки или юнит-тестирования нас в универе не научили от слова вообще… да и сейчас студентов таким вещам не учат.

Универ офигенен для социалочки. Девушки, пьянки, свободная жизнь в общежитии. Все вот это. Я на 3 курсе посетил дай сотона 50% лекций, а то и того меньше. Были дни когда мы по 4 часа в день играли в теннис. Смотрели фильмы (DVD, тогда еще), играли в игры, я прочитал тонну кних R A Salvatore на своем выигранном PDA (мы заняли 2 место на конкурсе программирования Barclays Capital Programming Challenge), ну и всякие MtG/D&D в больших количествах.

Для социалочки хорошо, а для развития все это какой-то фарс. Да и в исследованиях там сплошной фарс, обман и бессмыслица. Так то. ■

P.S.: тем временем…

Written by Dmitri

28 марта 2019 at 1:11

Опубликовано в Education

Кластер для финансового анализа

12 комментариев

В предыдущем посте (посту?) я показал свой комп, но проблема конечно этим не решается. Даже если учесть что у меня сейчас 16×3=48 ядер, этого хватает для того чтобы что-то считать, гонять TeamCity (видите какой я лояльный?) и еще кое-какую сервисную муть. Но этого мало, поэтому касательно вычислительной мощи у меня припасен другой козырь, про который я вам и расскажу.

Когда я ушел из аспирантуры в 2006 году, я не то чтобы уволился. Ну вот не уволился и все тут. Остался на позиции Visiting Researcher (или просто Visitor, чтоб без пафоса) в универе. Зачем? Ну, для начала плюшки простые: я получил от универа кучу софта, доступ в библиотеки (попробуйте поищите статьи в журналах не имея подписки), и другие мелкие плюшки вроде возможности получать академический софт и покупать железки от Altera со скидкой.

Но главное не это. Как я уже говорил, когда я начал заниматься алготрейдингом, я был нищий и вычислительных мощностей у меня практически не было — я гонял нейросети на компе который был HTPC билдом с Пентиум М (ноутбучный!) на борту. Ну, чем богаты тем и рады.

Так вот, тут внезапно оказалось, что мой универ входит в некий консорциум, которому принадлежит второй по размеру кластер в Англии! Хотите глянуть на спеку кластера? Окей. На текущий момент она выглядит как-то вот так:

  • 750 нодов, каждый из которых оборудован двумя ксеонами по 8 ядер (E5-2670) и 64гб оперативки
  • Четыре “толстозадые” ноды с E5-4640 (т.е. 32 ядра) и 256Gb оперативки
  • 12 GPU нодов в каждом из которых по две теслы — сейчас это K20. Одна такая игрушка стоит $5k, что сейчас кажется копейками, но когда-то это было огого!
  • Порядка 10 Xeon Phi 5110P нод которые, как вы понимаете, уже морально устарели.

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

Когда я сунулся во все это я, вообщем-то, тоже был тот еще раздолбай: у меня не было навыков ни в численных дисциплинах ни в программировании. Но я-то знаю как замотивировать себя изучить новый предмет: написать видеокурс по нему! Что я и сделал, записав для Pluralsight курс по High-Performance Computing.

После попадания на кластер, я столкнулся с очевидными ограничениями в работе системы: все данные синхронизованы по GPFS (ухх, InfiniBand, мне б такое!), а поскольку у нод нет своих дисков, это накладывает некоторые ограничения на то, сколько можно всего обсчитать.

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

Кластер сам по себе управляется с помощью MPI. Межпроцессорное взаимодействие я реализую с помощью OpenMP — достаточно бесячего, но работающего кое-как стандарта. Помимо этого, я порой использую SIMD — либо явно (это конечно не особо желательно), либо путем использования оптимизаций компилятора и подстаивания своих циклов так, чтобы это можно было делать проще через #pragma simd и иже с ними.

Что касается собственно того что я запускаю там, в основном это рассчеты на С++ или MATLAB (там лицензия MDCS) которые, как правило, тестируют ту или иную рыночную теорию. Поскольку вычислительных ресурсов чуть более чем дофига, а платить за них не надо (в отличии от, например, Azure), я гоняю очень много спекулятивной или вычислительно-неэффективной фигни которая, вместо того чтобы использовать механизмы machine learning-а (всякий annealing там), пытается по сути брут-форсить анализ путем рассмотрения избыточных комбинаций данных. Это моя визитная карточка — вместо того чтобы делать все умно (и сложно), я делаю де факто Монте-Карло.

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

Спеки нового кластера меня порадовали еще больше — 464 ноды по 40 ядер и 192гб оперативки, особо мощные ноды (которых мало и для них нужно просить допуск — для меня не вариант), а также няшные GPU ноды — 10 нод по 4 GTX1080 и 10 нод по 2 Volta V100. Я пожалуй единственный, у кого есть и навыки программирования CUDA (да-да, по этой технологии я тоже курс сделал, по тем же причинам) а также подходящие задачи чтобы все это амортизировать.

Не будь всего этого добра, я бы не смог анализировать так много, и так быстро. Конечно, сейчас я уже могу позволить себе купить какое-то время в облаке, но согласитесь — бесплатный кластер, которым кроме тебя мало кто вообще пользуется — это просто шикарно. Я правда и пользу приношу — когда что-то в кластере идет не так, я засылаю ответственным людям инфу о поломке с репро-кейсами, что делает их работу чуточку проще. Это делает людей меньшими раздолбаями, т.к. они понимают: либо они чинят поломку здесь и сейчас, либо я пойду к вышестоящим инстанциям универа и их всех вызовут на ковер. А если чего все англичане и боятся, так это личного общения — особенно с руководством. Проще все сделать как Дмитрий хочет.

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

Written by Dmitri

5 февраля 2019 at 19:21

Опубликовано в Computation

Tagged with , , , , , ,

Пост про мой исследовательский компьютер

2 комментария

Меня неоднократно просили сделать видео компа, который используется для работы поэтому вот, пожалуйста, видосик который можно посмотреть:

А если кому нужна детальная спека, то вот она:

Общая инфа:

  • Thermaltake Premium Core W200 — корпус на 2 (!) компьютера
  • Система водяного охлаждения. Состоит из 100+ частей, так что детально описывать не буду
  • Tina x Maxkey SA Dolch, Kailh Box Jade
  • Logitech MX Master 2S :)

Правая сторона (клиент):

  • Corsair HX1200i
  • ASUS WS X299 SAGE/10G
  • Intel Core i9-7960X
  • Intel Optane SSD 905P 480Gb
  • G.SKILL TridentZ Series 64GB F4-3200C15Q-64GTZSW
  • 2× Nvidia GeForce RTX 2080 Founders Edition, NVIDIA NVLINK 4-slot

Левая сторона (сервер):

  • Corsair HX1200i
  • ASUS WS C621E Sage
  • 2× Intel Xeon Scalable Gold 6130
  • Samsung M393A4K40BB2-CTD 64Gb
  • Intel Optane SSD 905P 480Gb
  • 4× 10-терабайтников Western Digital Gold (WD101KRYZ)
  • LSI 9300 MegaRAID SAS 9361-8i (LSI00417)

…и куча всякой дополнительной минорной утвари (USB контроллеры и т.п.). В процессе сборки умер один PSU (вероятно брак), все остальное успешно работает.

Что осталось сделать:

  • Найти 6 мониторов с разрешением 4К и диагональю 24″ или менее… сложная задача! (кронштейн Ergotech Hex 3 over 3)
  • Перебазировать жетские диски со старого компьютера
  • Перенастроить базы и роботов на новой системе
  • Смастерить для этих компьютеров самодельные бесперебойники из 18650? Возможно!
  • Перенастроить всю кухню связанную с FPGA на новом сервере. Может заодно купить какой-нть новый девайс :)

Мой предыдущий компьютер проработал 10 лет, проработал прекрасно, драйвил до 6 экранов на 1080р, проц на пассивном (!) охлаждении, вообщем о нем только хорошие воспоминания. Что он будет делать теперь — еще не знаю.

Компьютер выше собрал мне мой коллега. Сборка заняла более полугода — титанический труд, за который я премного благодарен! ■

Written by Dmitri

29 января 2019 at 2:01

Опубликовано в Technology

Tagged with , , , , , ,

Хранение и анализ биржевых данных

17 комментариев

Я должен признаться, что когда только начал общаться с коллегами на тему алготрейдинга, инфраструктуры у меня было где-то ровно нуль. Если мне не изменяет память, у меня было меньше 3-х мониторов (сейчас это немыслимо), да и комп был достаточно сомнительный. Поэтому, когда коллега отгрузил мне full order log Московской Биржи за несколько лет, мне пришлось пойти в магазин и купить пару «терабайтников» чтобы все это где-то хранить.

Полный ордер лог оказался просто огромным набором баз SQLite. На каждый день есть две основные базы — ордера и сделки. Естественно что формат этих баз «в лучших традициях». Вот например, допустим вам нужно записать в базу цену актива. Какой вы тип выберете? Правильный ответ тут — конечно же int, целое число, ведь цена имеет фиксированную точность далее которой не имеет смысла копать. Брать float/double нельзя т.к. там сразу теряется представление данных (зато в финмате у нас очень много где именно FP). Что же выбрала биржа?

Не, ну че, нормально так. Это даже не строка, это bcd_char(16,5), хотя я ввиду ограниченности драйвера который я использую вычитываю эту штуку именно как строку и потом просто конвертирую ее в соответствующий тип. Бонусные очки даются если угадаете в какой тип (намек: мы не можем полностью гарантировать сколько цифр будет после запятой, а FP теряет точность…).

Естественно что проблем вычитывания базы намного больше чем со странным представлением цен. Чего только стоит конверсия времени из varchar(22). Я может был бы рад хранить эту информацию в формате posix time, но проблема в том что для рассчетов это катастрофически неудобно, поэтому в коде порой появляются полностью нечитабельные перлы вроде этого:

static uint64_t netEpochOffset = 441481536000000000LL;
static ptime netEpoch(date(1400, 1, 1), time_duration(0, 0, 0));
static int64_t TimeToTicks(ptime time)
{
  time_duration td = time - netEpoch;
  uint64_t nano = td.total_microseconds() * 10LL;
  return nano + netEpochOffset;
}

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

Итак, естественный вопрос, а что же мы собственно хотели от всей этой кухни? На самом деле, обработка данных с биржи — это поэтапная процедура.

Шаг первый — это объединение разрозненных файлов в одну базу. Просто потому что с этим проще работать. Тут уже начинаются проблемы с консистентностью того, что дает биржа. Плюс к этому, начинаются различные вопросы оптимизации, т.к. база на несколько терабайт по определению не может работать быстро если вам нужно выборочно из нее что-то выдергивать. Но именно тут появляются две очень полезные возможности:

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

  • На базе можно производить индексирование и реорганизацию структуры.

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

Следующий этап после того как вы собственно получили базу данных (ордеров, допустим) — это попытка выудить цену актива. Цены, как вы понимаете, формируется из бидов и асков, но у вас их нет! У вас есть только заявки в стакане, и соотственно чтобы получить эту заветную цену, нужно для каждого нужного вам инструмента сформировать стакан и «дотянуть» этот стакан до того момента, когда вам нужна цена.

Естественно что иногда сама цена тоже недостаточна и хочется заниматься «микроструктурным анализом» — звучит пафосно но на самом деле этот анализ в основном делается визуально нежели алгоритмически (я работаю над этим!). В этом случае наличие стаканов тоже полезно, но вот сериализовывать стаканы для последующего использования я не считаю нужным т.к. это порождает невменяемый переизбыток данных. Но с другой стороны, работа с тиковыми данными очень болезненна, и эту проблему, похоже, не залить никакими деньгами.

Самый банальный пример анализа по этому делу: это когда ты строишь портфель на основе собственных рассчетов волатильности «я ж умнее всех» и потом хочешь дотянуть этот портфель до экспирации. В этом случае можно сделать ставку на то что рынок достаточно-ликвиден (тут все зависит от гранулярности, конечно, надо перепроверять) чтобы в любой момент можно было делать хэджирование с теми объемами которые нужно. Тут мы либо берем среднюю по больнице цену на краях стакана, либо строим стаканы ограниченной глубины. И конечно это не тиковые данные.

Раз нам нужны не тиковые данные, мы делаем дискретизацию или «квантизацию»: один раз все же строим полный стакан в памяти, но сэмплим этот стакан, например, раз в секунду, минуту, час и так далее. Если вам хочется держать руку на пульсе и следить за перепадами цен, которые могут цепануть ваши роботы, можно вместо краев или среднего (bid+ask)/2 брать high/low/open/close, опять же, все зависит от ваших допущений относительно волатильности.

Да, насчет волатильности: это отдельная тема т.к. в отличии от одного инструмента, эта штука сразу цепляет целый стрип опционов, и тем самым приходится анализировать намного больше данных. Опять же, можно записывать показатели выведенной из рынка волатильности, которая дискретизирована, но в этом случае вы естественно теряете надежду на всякий арбитраж, т.к. вы его просто не заметите. Но на рынке очень редко бывает когда улыбка волатильности отходит от интерполированных методом координатного спуска значений (и сам метод этот иногда приносит сюрпризы в форме ласточки вместо улыбки) поэтому что тут ловить напрямую — непонятно.

Резюмируя, обработка данных подразумевает как наличие хороших мощностей для хранения и вычислений, т.к. и проработанной инфраструктуры для того чтобы быстро и эффективно получить те или иные данные. Поэтому серьезные конторы вроде GS делают свои SecDB/Slang, а нам простым смертным остается только на коленке писать какие-то небольшие скрипты чтобы хоть что-то посчитать. Но к счастью, порой, и этого хватает. ■

Written by Dmitri

16 января 2019 at 14:51

Опубликовано в Data

Tagged with , , , ,

Мой подход к инженерии

6 комментариев

Вы может заметили что у меня в блоге нет ничего связанно с «хорошими практиками», DDD, «чистым» кодом и всем вот этим. И я если честно не особо фанат обсуждать все эти темы потому что считаю их природно-нерелевантными. Каждый раз когда мне кто-то в социалочке начинает заливать что «твой подход некомильфо», у меня мысль одна — если вы, ребята-айтишники, все такие умные и все знаете, почему вы такие бедные?!

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

Кодогенерация

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

Знаете есть такая шутка — «пиши код как будто следующий мейнтейнер будет агрессивным психопатом который знает где ты живешь». Так у меня есть свой вариант — «пиши код так будто тебе завтра нужно будет его оборачивать в SoC решение». В моем случае, SoC это когда у тебя есть, например, PCI плата с ethernet портом, а на плане как FPGA так и какой-нибудь ARM, и хочется чтобы все это работало быстро и в гармонии.

Две вещи которые я точно генерю почти всегда — это перечисления и сущности.

Перечисления нужно делать потому что реализация перечислений (enum-ов) в современных ООП языках это кошмарный ад. Кто и по каким причинам сделал все так криво я не знаю, но я обычно выношу перечисления в отдельный Т4 файл, который генерит все перечисления в проекте сразу, одним файлом вне зависимости от языка (хз работает ли это в Java но я не пишу на Java).

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

enum class Rarity
{
  Common,
  Uncommon,
  Rare,
  MythicRare,
};
std::ostream& operator<<(std::ostream& os, const Rarity value)
{
  string result;
  switch (value)
  {
    case Rarity::Common: result = "Common"; break;
    case Rarity::Uncommon: result = "Uncommon"; break;
    case Rarity::Rare: result = "Rare"; break;
    case Rarity::MythicRare: result = "Mythic Rare"; break;
  }
  return os << result;
}

Из кода выше понятно какой фигней я занимаюсь в свободное время. Но суть всего этого простая: иногда хочется воспринимать перечисление как некий токен с набором метаданных, к которым можно достучаться без лишнего шаманства вроде дотнетных атрибутов. Ну или наоборот, я могу передумать и сделать все на #define-ах.

Сущности, то есть простые классы в стиле POCO (plain old C/C#/C++ object) можно было бы тоже писать руками, но на самом деле нет. И суть вовсе не в штучках вроде паттерна наблюдатель (INotifyPropertyChanged и иже с ним, если мы про дотнет), а в том что очень часто степерь ахтунга обычной структуры идет over 9000, у меня кончается терпение чтобы сделать все через ООП, а делать АОП я не хочу потому что это черный ящик который плохо дебажится.

Хотите посмотреть на лютый ппц из сферы M:tG? Окей. Вот так примерно может выглядеть сущность под названием Mana:

struct Mana
{
  int32_t red{0}, green{0}, black{0}, white{0}, blue{0};
  int32_t  two_red{0}, two_green{0}, two_black{0}, two_white{0}, two_blue{0};
  int32_t colorless{0};
  bool is_red() const { return red > 0 || two_red > 0 || blue_red > 0 || black_red > 0 || red_green > 0 || red_white > 0;
  bool is_green() const { return green > 0 || two_green > 0 || black_green > 0 || red_green > 0 || green_white > 0 || green_blue > 0;
  bool is_black() const { return black > 0 || two_black > 0 || white_black > 0 || blue_black > 0 || black_red > 0 || black_green > 0;
  bool is_white() const { return white > 0 || two_white > 0 || white_blue > 0 || white_black > 0 || red_white > 0 || green_white > 0;
  bool is_blue() const { return blue > 0 || two_blue > 0 || white_blue > 0 || blue_black > 0 || blue_red > 0 || green_blue > 0;
  int32_t cmc() const { return colorless + 
    red + two_red + 
    green + two_green + 
    black + two_black + 
    white + two_white + 
    blue + two_blue; 
  }
  std::string str() const
  {
    string result;
    for (int i = 0; i < red; ++i) result += "R"; 
    for (int i = 0; i < green; ++i) result += "G"; 
    for (int i = 0; i < black; ++i) result += "B"; 
    for (int i = 0; i < white; ++i) result += "W"; 
    for (int i = 0; i < blue; ++i) result += "U"; 
    for (int i = 0; i < white_blue; ++i) { result += "{WU}"; }
    for (int i = 0; i < white_black; ++i) { result += "{WB}"; }
  // и еще дофига такого же ада⋮

Вообще, все что можно кодогенерировать я генерирую. Это упрощает задачу потом переводить это на новые парадигмы. Хоть какой-то контоль за процессом, т.к. в обычной ООП разработке контроля никакого нет, ой, мы сломали все тесты, бежим переписывать.

Да, еще много кодогенерации у меня делается из формул (MathSharp) и из Excel (X2C). Там тоже все очень няшно: помимо математических оптимизаций, можно еще делать хитрые численные перестановки, например выбирать между float и double, делать обработку ошибок и диапазонов. Кстати! Формочки для ввода данных тоже создаются автоматом.

Короче, что я хочу сказать? Что у меня код местами забросан вот всем этим адом, при этом поскольку это сгенерированный ад, я еще могу на него всякие интересные тесты генерить, плюс автогенерация — это бесплатные оптимизации в стиле «давайте засунем несколько байт в один int» (например, на GPU иногда приходится так делать).

Кодогенерация — это не nuclear option, это вообще норма.

Dropbox для всего и вся

У нас очень модно использовать всякие системы вроде Git/GitHub, но я не фанат. Вообще все, что у меня есть, лежит в Dropbox, который просто синхронизируется везде и всегда.

Какой в этом смысл? Ну, я всегда знаю где что лежит. На разных системах одна и та же программа будет писать в один и тот же путь, который засинхронизируется на всех компах. Например, если у вас один из компов настроен гнать юнит-тесты из определенной директории, он всегда будет получить обновленные файлы оттуда и гнать их. Это круче чем сигналы, которые сорсконтрол якобы посылает ТимСити и подобным.

Когда мне нужно синхронизировать системы по Dropbox, я использую… токен-файлы! Прямо как Microsoft Office. Просто создаю например файл в котором написан таймстэмп, другой комп на него смотрит и принимает решения. Брутально но работает.

Единственное чего не записать в Dropbox это биржевые данные. Их чуть более чем дофига, и синхронизировать все это не особо нужно. Обычно идет это на RAID, а вот уже некоторые обработанные куски данных можно держать в дропбоксе, хотя… на самом деле анализ все равно происходит не локально.

Да, кстати насчет этого. Как я уже говорил, у меня исследовательская позиция, которая «активна» года этак с 2002… ухх, много воды утекло. Зачем мне она? Ну потому что там есть нереально мощный кластер. Иногда мне кажется что я единственный во всем универе кто реально умеет этим кластером пользоваться: на самом деле есть студенты которые че-то шарят в HPC, но они в основном делают курсовые. Вообщем много моего анализа происходит там, но там другая среда: там Linux, InfiniBand, конечно синхронизация файлов между нодами, а для оркестрации используется MPI. Ну, тоже, как вы понимаете, в основном плюсы. И Dropbox конечно там не применим — я просто перекидываю туда свои «долгие» задачи. Это отдельный рассказ, там много тонкостей и не думаю что они кому-нибудь интересны.

Центральный текстовый репо

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

У меня другой подход. У меня есть директория, в которой находится более менее организованная свалка всякого добра которое нужно в разных проектах. Когда приходит время, я просто физически линкую нужный мне файл в проект, тем самым импортируя его не как бинарную либу или .h/.lib а как текст!

Вообще, шаринг кода и либ это нерешенная проблема. Мне уже если честно надоело пересобирать по ночам Boost или QuantLib — конечно, это делает мой CI, но постоянно возникают какие-то косяки, новые флаги компиляции, новые блин компиляторы которые ломают код.

Intel C++ Compiler

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

Отдельно стоит упомянуть «игрушки» под названием Xeon Phi, которые Intel сначала выпустил как PCI карточки, а сейчас делает их как просто процессоры (делает ли?). Для них, несмотря на x86 совместимость, нужна перекомпиляция интелевским компилятором. Phi — это полумертвая технология, но мне она все равно нравится потому что засунуть комп в комп это всегда круто. Пусть себе работает, 60 ядер, никаких проблем с branch divergence в отличии от CUDA.

CUDA Toolkit

CUDA позволяет считать некоторые data-parallel вещи на GPU. На ней лучше всего считать то, что вы считаете постоянно и регулярно, т.к. разработка сама по себе более затратна и проблемы тут тоже бывают. Естественно, что задачи где много conditional logic лучше тут не гонять, хотя как знать, если правильно дробить, можно все равно получить хороший прирост.

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

Что меня больше всего удивляет в куде так это NSight, система удаленной отдадки. Это гениально т.к. твой GPU не должен находиться локально.

Плохой код

Не хочу сказать «говнокод» (все не настолько плохо), но все же я с овновном пишу плохой код который как-то работает. Суть в том, чтобы быстро тестировать идеи, т.к. априори понимаешь, что большинство из них провальные. Ну и не будем забывать что время которое нужно чтобы прогнать тест иногда совсем не нулевое.

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

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

Я тут подчеркну, что я не считаю все эти шаги и действия «скиллами программирования»… это common sense. Ты хочешь сделать все чуть более упорядоченным, снизить энтропию. Мне, к счастью, не нужно например чтобы код кем-то читался. Я использую сокращения которые остаются у меня в голове, я использую нестандартные фичи языка когда это удобно. По крайней мере, в отличии от F#, я могу спокойно вернуться в свой код через пару месяцев и все более менее понятно.

Вот такая вот «инженерная культура». Или ее отстутвие. ■

Written by Dmitri

19 декабря 2018 at 23:13

Опубликовано в Engineering