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

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

Использование ImpromptuInterface в реализациях паттернов проектирования

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

Вы наверняка уже знаете, что если отнаследоваться от DynamicObject, можно получить разные плюшки от DLR, такие как например возможность вызывать методы класса даже если у класса их нет, и все такое. Но знаете ли вы что можно «вывесить» этот ваш DynamicObject как абсолютно любой интерфейс? Зачем это надо, спросите вы — ну, для того чтобы реализовывать всякие паттерны проектирования. Вот парочка примеров.

Как это работает?

После добавления NuGet пакета ImpromptuInterface, у вас появится extension-метод ActLike<T>(), который будет доступен на всех объектах типа object. Параметр T в данном случае – это именно тип интерфейса который хочется вывесить наружу.

За кулисами этого вызова создается, конечно же, динамическая прокся, которая является одновременно и проксёй для оригинального объекта и при этом реализует нужный интерфейс.

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

Паттерн Null Object

Самый простой пример — это паттерн Null Object, т.е. объект, который ровным счетом ничего не делает.

Вот например, у вас есть интерфейс логирования

public interface ILog
{
  void Info(string msg);
  void Warn(string msg);
}

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

public class BankAccount
{
  private ILog log;
  private int balance;
  public BankAccount(ILog log)
  {
    this.log = log;
  }
  public void Deposit(int amount)
  {
    balance += amount;
    // check for null everywhere
    log?.Info($"Deposited ${amount}, balance is now {balance}");
  }
  public void Withdraw(int amount)
  {
    if (balance >= amount)
    {
      balance -= amount;
      log?.Info($"Withdrew ${amount}, we have ${balance} left");
    }
    else
    {
      log?.Warn($"Could not withdraw ${amount} because balance is only ${balance}");
    }
  }
}

Представьте теперь, что логирование вам нафиг не сдалось. Что делать? Передавать null нельзя, будут NRE везде где вызовы, ведь safe call operator ?. повсеместно разработчики, как правило, не используют. Ждем non-nullable types в C#8 или позже.

Один вариант — просто сделать фейковый класс:

public sealed class NullLog : ILog
{
  public void Info(string msg)
  {
    
  }
  public void Warn(string msg)
  {
    
  }
}

Это и есть «каноническая» реализация паттерна Null Object, но она получается легко только когда членов у класса мало, или же ReSharper под рукой. В целом, не хочется писать всю эту муть, особенно если вас не так сильно волнует перформанс.

Итак, третий, «ядерный» вариант с использование ImpromptuInterface. Делаем класс со следующей сигнатурой:

public class Null<T> : DynamicObject where T:class
{
  ⋮

T параметр выше – как раз тип интерфейса. Теперь можно сделать из класса этакий псевдо-singleton (на самом деле нет, создаем каждый раз):

⋮
public static T Instance 
{
  get
  {
    if (!typeof(T).IsInterface)
      throw new ArgumentException("I must be an interface type");
    return new Null<T>().ActLike<T>();
  }
}
⋮

Магия в том, что мы делаем Null<T>, сохраняя тип аргумента, но вывешиваем его не как Null<T> а просто как T.

А что дальше? Дальше нам нужно перехватывать вызовы методов, например:

  ⋮
  public override bool TryInvokeMember(InvokeMemberBinder binder, 
    object[] args, out object result)
  {
    result = Activator.CreateInstance(binder.ReturnType);
    return true;
  }
}

Код выше смотрит на тип возвращаемого значения метода и берет его дефолтную реализацию, т.е. вызывает new T() через System.Activator — это конечно же чревато тем, что если у класса нет дефолтного конструктора, то result будет равен null что очень грустно но не критично если вы не будете пытаться этим результатом пользоваться.

Вот собственно и всё, наш null object готов:

var log = Null<ILog>.Instance;
var ba = new BankAccount(log);
ba.Deposit(100); // логирования не будет

Динамическая Прокси

А теперь давайте представим «обратный» сценарий: у вас есть класс банковского счета, вы хотите добавить логирование «со стороны».

public interface IBankAccount
  {
    void Deposit(int amount);
    bool Withdraw(int amount);
    string ToString();
  }
  public class BankAccount : IBankAccount
  {
    private int balance;
    private int overdraftLimit = -500;
    public void Deposit(int amount)
    {
      balance += amount;
      WriteLine($"Deposited ${amount}, balance is now {balance}");
    }
    public bool Withdraw(int amount)
    {
      if (balance - amount >= overdraftLimit)
      {
        balance -= amount;
        WriteLine($"Withdrew ${amount}, balance is now {balance}");
        return true;
      }
      return false;
    }
    public override string ToString()
    {
      return $"{nameof(balance)}: {balance}";
    }
  }

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

public class Log<T> : DynamicObject where T : class, new()
{
  private readonly T subject;
  private Dictionary<string, int> methodCallCount =
    new Dictionary<string, int>();
  protected Log(T subject)
  {
    this.subject = subject;
  }
  ⋮

Далее можно сварганить фабричный метод, который будет делать тип Log<T> и выдывать его как интерфейс I. Например:

⋮
public static I As<I>() where I : class
{
  if (!typeof(I).IsInterface)
    throw new ArgumentException("I must be an interface type");
  return new Log<T>(new T()).ActLike<I>();
}
⋮

Теперь нам нужно сделать так, чтобы при вызове метода, метод-то вызывался, но кол-во методов тоже записывалось. Для этого нужно перегрузить TryInvokeMember():

⋮
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
  try
  {
    // logging
    WriteLine($"Invoking {subject.GetType().Name}.{binder.Name} with arguments [{string.Join(",", args)}]");
    // more logging
    if (methodCallCount.ContainsKey(binder.Name)) methodCallCount[binder.Name]++;
    else methodCallCount.Add(binder.Name, 1);
    result = subject.GetType().GetMethod(binder.Name).Invoke(subject, args);
    return true;
  }
  catch
  {
    result = null;
    return false;
  }
}
⋮

Что делает код выше? Ну, помимо логирования количества вызовов, он через reflection вызывает нужный метод на объекте и сохраняет результат. Если вдруг падает исключение — что поделать, бывает, пишет в результат null, и всё.

Ну и наконец, если вам вдруг надо чтобы работал честный ToString() на объекте, но он декорировал существующий ToString() того типа, на котором ведется логирование, то это можно сделать вот так:

  ⋮
  public string Info
  {
    get
    {
      var sb = new StringBuilder();
      foreach (var kv in methodCallCount)
        sb.AppendLine($"{kv.Key} called {kv.Value} time(s)");
      return sb.ToString();
    }
  }
  
  // will not be proxied automatically
  public override string ToString()
  {
    return $"{Info}{subject}";
  }
}

Вот собственно и всё, теперь можно юзать вот эту проксю как-то вот так:

var ba = Log<BankAccount>.As<IBankAccount>();
ba.Deposit(100);
ba.Withdraw(50);
WriteLine(ba);

Заключение

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

Если вам интересны паттерны, у меня есть более 10 часов видосиков по паттернам в C#/.NET. Enjoy!

Written by Dmitri

4 мая 2017 at 12:41

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

.NET-ный enum в стиле Rust

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

Как вы уже наверное знаете, у разных языков (C#, Java, Rust, …) понимание того что такое enum (перечисление) абсолютно разное. Иногда мне в C# хочется чтобы было как в Rust, да и в C++ тоже. Поэтому это рассказ про то, как сделать «хорошо» на примере C#.

Однородный тип

Давайте начнем. Например, чтобы писать

Color color = Color.Red;

нужно чтобы Color.Red имел тот же тип что и Color. То есть, если утрировать, можно и так написать:

public class Color
{
  private byte r, g, b, a;
  public Color(byte r, byte g, byte b, byte a)
  {
    this.r = r;
    this.g = g;
    this.b = b;
    this.a = a;
  }
  public static Color Red = new Color(255, 0, 0, 255);
}

Вопрос теперь только в том как генерить все это многообразие. В Rust ничего особо писать не надо, а в C#, как видите, нужно сделать очень много телодвижений. Но результат практически тот же.

Что-то мы потеряли, а что-то приобрели. Потеряли мы все Enum.GetValues(), т.е. возможность получить все предопределенные цвета. Как их вернуть? Ну наверное как-то вот так:

public static Color Red = new Color(255, 0, 0, 255);
public static Color Blue = new Color(0, 0, 255, 255);
public static Color[] Values = new[] {Red, Blue};

Что ещё продолбано? Ну, имена этих переменных. Ой! А вот это непросто будет сделать:

private BiDictionary<string, Color> colors = new BiDictionary<string, Color>
{
  ["Red"] = Red,
  ["Blue"] = Blue
};
public IEnumerable<string> Names => colors.Keys;
public IEnumerable<Color> Values => colors.Values;
public Color this[string name] => colors[name];
public string this[Color color] => colors.Reverse[color]; // возможно излишне?
private string ToStringImpl()
{
  return $"{nameof(r)}: {r}, {nameof(g)}: {g}, {nameof(b)}: {b}, {nameof(a)}: {a}";
}
public override string ToString()
{
  if (colors.Reverse.ContainsKey(this)) return colors.Reverse[this];
  return ToStringImpl();
}

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

Итак, всё ли мы реализовали? Да вроде да, можно использовать

Color red = Color.Red;
Color my = new Color(255, 0, 255, 0);

и вывод будет вот такой:

Red
r: 255, g: 0, b: 255, a: 0

Дискриминированное объединение

Пока все просто, давайте пример посложнее уже. Типичный пример вычисления со всякой неоднородной начинкой выглядит как-то так:

type IntOrBool =
  | Boolean of bool
  | Integer of int // это не раст, это F#

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

public abstract class IntOrBool
{
  public int Tag => this is Integer ? 0 : 1;
  public bool IsInteger => this is Integer;
  public bool IsBoolean => this is Boolean;
  public static Boolean NewBoolean(bool value) => new Boolean(value);
  public static Integer NewInteger(int value) => new Integer(value);
}

Ну и соответственно каждый из наследников должен выглядеть как-то вот так:

public class Integer : IntOrBool
{
  public int Value { get; set; }
  public Integer(int value)
  {
    Value = value;
  }
  // делаем вид что мы int
  public static implicit operator Integer(int value)
  {
    return new Integer(value);
  }
  public static implicit operator int(Integer integer)
  {
    return integer.Value;
  }
}

Но дальше у нас те же проблемы что и ранее — как сделать ToString(), как вывести список всех возможных типов объединения? Мы точно знаем как это делает F# — он для этого использует атрибуты, и по сути поиск всех классов превращается в использование Reflection. Что уныло. А что если сделать вот так?

public static Type[] Cases = new[] {typeof(Boolean), typeof(Integer)};

В конечном счете, что мы теряем? Ладно, пора уже попробовать использовать все это в типичном (насколько это возможно для такого синтетического примера) сценарии:

IntOrBool foo = 0; // FAIL :(

Опа, уже нифига не работает. Хотели удобство а получилось не очень. Что, еще операторов? Пожалуйста

public static implicit operator IntOrBool(int value) => NewInteger(value);
public static implicit operator IntOrBool(bool value) => NewBoolean(value);

ок, теперь можно инициализировать, а можно ли проверять if-ом?

IntOrBool foo = 0;
if (foo is Integer bar)
{
  int i = bar;
  WriteLine(i);
}

Кривоватенько, но работает. foo is int bar конечно же писать нельзя. Ну и выборку по шаблону в стиле case Integer(i) пока тоже в C# не сделали.

Да, насчет ToString() — тут будет просто GetType().ToString() для кейсов у которых нет собственных данных. То есть будут порождаться типы даже там, где они не нужны. Для F# это как бы норма (в F# например, любой переданный оператор превращается в struct), но в C# это не очень хорошо, хотя опять же, ничего критичного.

Вообщем вот такой финт ушами, не дающий особых плющек, т.к. ко всему этому нужен умный switch и полноценный pattern matching. Да, и Deconstruct(), если вы его реализуете, не поможет для типов которые имеют один member. Сорри! ■

Written by Dmitri

14 апреля 2017 at 0:47

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

Tagged with

Работа лето интерн C++ программист студент графика :)

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

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

Система моя называется Kinetica, она написана на C++ с использованием Direct2D/DirectWrite и FFMPEG. По сути, это программа, которая просто генерит анимированный текст (в видеофайлах), и мне нужен кто-то, кто

  • Поанализирует мои запросы на разные механизмы анимации

  • Напишет код который эту анимацию сделает (хочется красивое API)

  • Напишет какие-нибудь тесты для всего этого

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

Касательно навыков, от вас требуется

  • Знание С++ и как на нем делать приличное ООП

  • Навыки работы с Visual Studio

  • Некоторое понимание компьютерной графики

  • Умение работать с Git (я использую GitLab)

  • Умение работать с баг трекером

Вообщем если вы студент, учили С++ и думаете «а зачем мне оно надо», то тут есть шанс погенерить интересные видео, поднять скилы, и все такое. Работа фултайм, задач будет много — как на создание красивого API, так и на создания на этом API нужных мне анимаций. Которых будет много.

Деньги — самое главное. $1k/месяц пейпалом. Работа, как вы поняли, удаленная.

Вопросы, пожелания, пишите, отвечу что да как.

Written by Dmitri

16 марта 2017 at 1:10

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

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

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

Ну что, всех можно поздравить с релизом VS2017? Для начала, я пожалуй признаюсь что я давно уже (время наверное на месяцы пошло) использую Rider (и даже запилил по нему видеокурс), но релиз VS2017 — это не только релиз слегка медленной, 32-битной Windows-only IDE которая даже не умеет правильно рисовать лигатуры. Это еще и релиз новых версий компиляторов — именно того, что Microsoft нам обещали делать out-of-band, но обещание оказалось наглой ложью, как для C++ так и для C#.

Что же, давайте посмотрим что там в новом сишарпике…

Локальные методы

Вообще, проблемы с локальными методами толком не было, т.к. в любой функции можно декларировать лямбда-переменную, что и есть по сути метод. Так что в C#7 это разве что привели в более удобоваримый вид:

public static Tuple<double,double> Solve(double a, double b, double c)
{
  double CalculateDiscriminant()
  {
    return b * b - 4 * a * c;
  }
  var disc = CalculateDiscriminant();
  return Tuple.Create(
    (-b + Math.Sqrt(disc)) / (2 * a),
    (-b - Math.Sqrt(disc)) / (2 * a)
  );
}

Что мы можем заметить в примере выше? Правильно, вложенная функция умеет захватывать окружение, в т.ч. аргументы внешней функции. И конечно же дать функции CalculateDiscriminant аргументы a,b,c не получилось бы, т.к. эти имена уже заняты.

Ложка дегтя — расположение метода в другом методе может быть где угодно, то есть можно сначала использовать а потом задекларировать. Это на любителя, конечно, но тут главное — прямые руки и здравый смысл.

Касательно полезности фичи: мне норм. Этот подход позволяет плодить меньше членов на уровне класса, а баловаться с лямбдами напряжно т.к. для любой лямбды нужно определить тип Func<int,Foo,string> и это порой бывает утомительно — хотя в некоторых случаях Решарпер помогает. Вообще было бы лучше если бы нам позволяли еще делать локальные статические переменные прямо в методах, как это позволяет делать C++.

Out-переменные

Обычно как было: декларация переменной это statement. Все, теперь это по сути expression т.к.:

string s;
if (DateTime.TryParse(s, out DateTime dt))
{
  // используем dt
}

Это чисто «сахарная» фича, и резонным будет вопрос — а что если DateTime не пропарсится а мы все равно попытаемся ей воспользоваться? Ну, тут все просто — у этой штуки будет дефолтное значение. Но зато заметьте, dt имеет тот же scope, что и строка s.

Кортежи

Кортежи уже были в C#, но вставлены они были криво: все эти Item1, Item2 не добавляли к удобству пользования, и кому-то могло показаться что рано или поздно нам вообще подсунут variadic templates. Но разрабы поймали себя и все-таки сделали что-то годное:

public static (double x1, double x2) Solve(double a, double b, double c)
{
  var disc = CalculateDiscriminant();
  double CalculateDiscriminant()
  {
    return b * b - 4 * a * c;
  }
  return (
    (-b + Math.Sqrt(disc)) / (2 * a), 
    (-b - Math.Sqrt(disc)) / (2 * a)
  );
}

И вызвать можно это вот так:

var (x1, x2) = QuadraticEquationSolver.Solve(1, 10, 16);
Console.WriteLine(x1 + ", " + x2);

Ну а если декларировать кортежи на месте, то правила те же самые, и есть варианты — давать элементам кортежа имена или нет:

var names = (first: "Dmitri", last: "Nesteruk");
Console.WriteLine(names.first);

Как видите, кортеж определяется как тип через скобочки и список типов (т.е. (double,double) условно-эквивалентно ValueTuple<double,double>). Но ложка дегтя тут огромная: для всего этого нужен внешний пакет System.ValueTuple. Что?!?!?

Спокойно, очевидно что просто эту тему недопилили и ValueTuple<T, ...>, который должен был быть в .NET 4.6.2 BCL просто туда не попал. Что это за класс? Да просто еще одна реализация кортежа, но в этот раз вся ее суть закрыта синтаксическим сахаром вроде того что я привел выше. Если кому интересно что под капотом, вот сорцы.

Expression-bodied members

На момент C#6, некоторые вещи можно было писать как expression bodies, т.е например void Foo(x) => x+1; делал вполне себе валидный метод. Сейчас же, это счастье распространилось помимо методов и пропертей еще на конструкторы/деструкторы, геттеры и сеттеры:

class Person
{
  Person() => Names = new[]{"Nameless One"};
  public int FirstName
  {
    get => Names[0]; // ну вы поняли
  }
}

Вообще вся эта expression bodied тематика экономит хорошо так символов. Единственное что мне не нравится это каша вроде

class Person
{
  bool CanVote => Age <= 16; // без лигатур - коряво :(
}

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

Throw Expressions

Тут мне 2017 студия осмелилась подправить Решарперный код. Было что-то вроде этого:

class Foo
{
  Foo(Bar bar)
  {
    if (bar == null) throw new SomeException();
    this.bar = bar;
  }
}

но теперь оказывается можно писать вот так

class Foo
{
  Foo(Bar bar)
  {
    this.bar = bar ?? throw new SomeException();
  }
}

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

Pattern matching

Когда-то я писал про самопальный pattern matching, но тут сделали реальную реализацию.

Начнем с простого — теперь проверку на тип можно сделать прямо в if-е и сразу получить и проверку и приведенный тип:

if (shape is Rectangle rc)
{
  var area = rc.Width * rc.Height;
}

Ну и потом нам разрешили делать switch на этом месте:

Shape s = new Rectangle { width = 0, height = 0 };
switch (s)
{
  case Rectangle rc when (rc.width == rc.height):
    Console.WriteLine("It's a square!");
    break;
  case Circle c:
    break;
}

Заметьте ключевое слово when выше. Ничего не напоминает? Правильно, F#. Но конечно F# все еще лидирует в этом плане. Хотя бы потому, что в F# можно делать алгебраические типы данных, что очень полезно когда ты, например, что-нибудь структурированное парсишь, например MathML.

Ну хорошо, смотрите, хоть что-то сделали. Конечно, нельзя мэтчить на Rectangle(42, double h) — это откинули на будущее. И switch на кортежах тоже пока не работает. Грусть-печаль.

Ref returns

Как вернуть элемент массива by reference? Ну строго говоря, можно запинить весь массив и попытаться вернуть указатель в unsafe. Но теперь есть более гуманный способ — ref на чем угодно.

int[] numbers = new int[] { 1, 2, 3 };
ref int refToSecond = ref numbers[1];
refToSecond = 123;
Console.WriteLine(string.Join(",",numbers)); // 1, 123, 3

Хмм, ну это хорошо конечно, но со списком такое не пройдет

List<int> numbers = new List<int> { 1, 2, 3 };
ref int second = ref numbers[1]; // A property or indexer cannot be used⋮

Хмм, да, полезность сей затеи стремится к нулю. Нельзя например сослаться на букву в строке.

Все это — простой aliasing, то есть еще одно имя для той же самой переменной. Это не «ссылка на область памяти» т.к. это обычно требует слово fixed чтобы вменяемо работать в unsafe контексте, а у нас все тут safe.

Да, теперь можно и из метода ref на переданный ref возвращать, то есть возможно вот такое мракобесие:

static ref int Min(ref int x, ref int y) {
  if (x < y)
    return ref x;
  else
    return ref y;
}
⋮
int a = 1, b = 2;
 ref int c = ref Min(ref a, ref b);
c = 123; // a == 123!!!

Holy s~t, посмотрите сколько тут раз ref написано. Даже в вызове используется ref Min(...) потому что если ты опустишь этот ref это значит что тебе не нужен референс на значение, а нужно само значение.

int d = Min(ref a, ref b); // вполне валидно, d == 1

Проблема тут в том, что этот ref — это то же самое что референсы в C++, из-за которых теперь в уютном сишарпике можно писать треш вроде Min(ref a, ref b) += 123 (по сути делает a += 123), что абсолютно отвратительно для читания и понимания. К тому же, неопытные программисты тут же будут ломиться и возвращать адреса локальных переменных, что делать нельзя:

static ref int Foo()
{
  int x = 0;
  return ref x; // не скомпилируется - cannot return local by reference
}

Хотя помнится что году так в 2012, Липперт угрожал что они могут и этот сценарий поддержать.

Что еще тут?

Теперь можно делать подчеркивания в литералах: int x = 123_456;. Также появились бинарные литералы — теперь можно писать var literal = 1100_1011_____0101_1010_1001;. Подчеркиваний может быть сколько угодно.

Pattern matching — все самое вкусное не попало в C#7, ждем следующих версий. Должны быть всякие вот такие штуки поддержаны:

switch (foo)
{
  case 123: ⋮
  case MyEnum.Stuff: ⋮
  case string s: ⋮
  case Point(int x, 321) where x > 0: ⋮
  case Point(12, 23) : ⋮
  default: ⋮
}

Record types — не вошли, будут в C#8 наверное.

Ну вообщем как-то так. А вы заметили насколько шустрее стал компилятор C#7? Серьезно, видимо опять что-то подкрутили с инкрементальностью. Кстати, в VS2015 сделали обалденные улучшения инкрементальности C++, а тут кажется улучшили C#. У меня «горячая» компиляция летает, хотя «холодная» все еще тормозит как и раньше.

Наверняка что-то еще из фич забыл, напишите в комментариях. ■

Written by Dmitri

13 марта 2017 at 10:54

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

Итоги 2016 года

leave a comment »

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

oyz5vst5-me1

Но если серьезно, я все же повторюсь. Год был офигенен! Особенно после того, как я в конце мая прекратил работать евангелистом — это потеряло финансовый смысл уже давно, и несмотря на то что оплаченный бизнес-класс в США чуть ли не каждый месяц это как бы хорошо, я пожалуй дать отказ, сорри. Мы кстати все равно на очень позитивных отношениях с конторой – я уже как минимум на 2-х эвентах там был, плюс я планирую сделать для PS курс по Rider когда он выйдет. JB много чего мне дал и, сугубо имхо, все еще одна из лучших контор для личностного саморазвития и путешествий за чужой счёт. А вот для кэша – это другой, менее приятный разговор :)

9zmq7ia_ygi1

Мои технологические взгляды остаются такими же, я все еще большой поклонник C++ и C#, оба языка прекрасно себя показывают на соответствующих задачах. Я начал серьезно заниматься FPGA разработкой (на VHDL, что возможно было не лучшей идеей), много чего изучил и в мечтах построить feed handler — для какой биржи и протокола – пока не знаю. Это сложная, запутанная дисциплина, и я приобрел массу уважения к людям которые занимаются этим не смотря на низкие, по сравнению с software engineering, зарплаты.

Подвижки в мире меня мало тронули: реакционные действия масс (Brexit и выборы Трампа) понять можно, т.к. люди устали от политкорректности и подлизывания ко всяким меньшинствам. В моем универе в Англии уже и петиции насчет gender-neutral туалетов, и “транс” студенты уже не то что дико, а это стало нормой. Тем временем, Германия и прочие страны (Меркель войдет в историю как Гитлер со знаком минус) напустили толпы иммигрантов, и теперь развал ЕС не за горами – как только Франция выберет Марин Ле Пен. Думаете не выберут? То же самое говорили про Трампа. А он выиграл. Продемонстрировал как один человек победил всю систему. Я это уважаю, т.к. сам индивидуалист и ратую за победу индивидуума над обществом.

ginc-s7n8ys1

На личном фронте, не смотря на мои подвижки в сторону Лаппеэнранты (я всегда презирал дачников, а тут вдруг нате), я планирую двигаться в сторону Эстонии. Для меня это хороший языковой компромисс, а также вынужденная мера, т.к. Британия пожет выйти из ЕС по-разному, и если она выйдет боком, то это и мне выйдет боком, увы. Как и в каком ключе у меня это получится — не знаю, но охота пуще неволи, так что поживем-увидим. Многие удивлены моему повороту дел и считают что я должен вернуться к трудовой жизни. Что ж, так уж и быть, но это будет по моим правилам. А чем я буду заниматься — это пока сюрприз, хотя немного предсказуемый. А в Лапу я все равно буду ездить — это очень близко к Петербургу, и думаю весной-летом там будет самое то. По крайней мере, надо же откуда-то брать нормальный сыр и рыбу. Хотя в Стокгольм тоже ездить буду периодически, хоть оно и подальше.

jawcaklu1u1

А еще в этом году я стал велосипедистом (но не велофанатом, это другая порода, менее адекватная) — началось все с того, что мы купили себе складные велосипеды, а закончилось тем, что я поехал в Стокгольм чтобы купить себе электронно-двухподвесный Crescent, на котором отныне гоняю по городу и пригородам в любую погоду. Жизнь на скорости 25км/ч безусловно поменяла меня, надеюсь что к лучшему. Но на этом фронте я не думаю увидеть какие-то перемены; наоборот — мне теперь нужно следующее хобби.

Ладно, пора резюмировать уже!

У меня есть подозрение что 2017 будет не просто очень хорош, а у меня, по крайней мере, будет качественный скачок: уход от мелочных, приземленных задач и реализация более серьезных проектов. Хотя я, ей-Сотоне, готов просто сидеть тут, пить Сотерн и слушать новый альбом Энигмы. И пусть весь мир подождёт…

С Новым Годом!!!

gzsdzwgdcvi1

Written by Dmitri

31 декабря 2016 at 23:59

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

Tagged with

Проект CallSharp: I/O Call Instrumentation на платформе .NET

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

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

callsharp_0_1_1

Сначала простой пример: у вас есть "abc", нужно получить "cba". Ниже я представил это схематично, и далее в статье я буду продолжать использовать такие заголовки.

ff0

Этот пример идеально иллюстрирует проблему, т.к. в .NET у строки нету метода Reverse(), и решений этой задачи – несколько. Например, можно написать вот так:

new string(input.Reverse().ToArray())

Следовательно, хотелось бы получить программу, которая сама выводила бы эти цепочки вызовов на основе входных и выходных данных, гуляя по .NET BCL API, делая все возмножные вызовы и проверяя их на соответствие. Звучит немного фантастично, да?

Давайте возьмем для начала простой пример:

ff1

В нашем случае abc – это точно строка, и ничто иное. Теперь нам предстоит понять что на abc можно вызвать чтобы получить ABC.

Нам повезло что строки в .NET немутабельны, и нам не нужно проверять изменения оригинальной строки после вызовов на ней – только выходного значения. А следовательно, мы можем взять и поискать все методы (а также свойства, которые в .NET тоже методы с приставкой get_), которые

  • Являются нестатическими методами класса string (System.String, если быть педантичными)

  • Могут не принимать ни одного аргумента

  • Возвращают строку

Примечательно, что «могут не принимать ни одного аргумента» – это три раздельных случая, а именно

  • Функция не имеет параметров вообще, т.е. Foo()

  • Функция может и имеет параметры, но у всех них есть дефолтные значения, т.е. Foo(int n = 0)

  • Функция берет упакованый список, т.е. Foo(params char[] letters)

Если руководствоваться этими критериями, мы получим список фунций string, которых было бы неплохо вызвать на строке "abc":

Normalize 
ToLower 
ToLowerInvariant 
ToUpper 
ToUpperInvariant 
ToString 
Trim
TrimStart
TrimEnd

Берем каждую из этих функций, вызываем на "abc", смотрим на результат. Подходят только две функции:

input.ToUpper()
input.ToUpperInvariant()

Ура, первая миссия выполнена!

ff2

Как понять, что за тип у числа 3 справа? Я предлагаю вот такой алгоритм:

  • Через reflection, берем все типы у которых есть метод TryParse().

  • Вызываем на всех данных. Если возвращает true – делаем боксинг распаршенного (неологизм?) объекта, возвращая его как object.

  • Не забываем, что любой ввод это как минимум string. А если ввод имеет длину 1, то это еще и char.

Согласно этому алгоритму, тройка (3) справа может быть и string и char (а также float или даже TimeSpan!), но в текущем примере, мы допустим что это все же Int32 или просто int.

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

input.Length

Естественно, что на самом деле это вызов функции get_Length(), но CallSharp заранее удаляет все ненужные декорации для удобства пользователя.

ff3

Читерский пример. Если бы я взял true, мне бы попался IsNormalized(), а так на не-статике вариантов нет. Что же, придется расширить наш алгоритм – теперь будем перебирать ещё и статические методы, которые

  • Не обязательно являются членами класса (в нашем случае – строки), но тем не менее попадают в список одобренных типов. Причина: я не хочу произвольно вызывать File.Delete(), например

  • Возвращают нужный нам тип (в данном случае – bool)

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

string.IsNullOrEmpty(input)
string.IsNullOrWhiteSpace(input)

Прекрасно! Давайте что-нибудь посложнее уже!

ff4

Ухх, тут ситуация посложнее – "abc ", то есть два пробела на конце: это одной функцией уже не получить. Надо делать цепочку вызовов. В данном случае цепочка не должна быть stringstringstring, она может быть stringчто угодноstring, т.к. промежуточные данные нам не важны.

Именно на этом этапе происходит комбинаторный взрыв. Ну, а что вы хотели? Зато мы на наших входных данных получаем очень много вариантов:

string.Concat(input.Split()).ToUpper()
string.Concat(input.Split()).ToUpperInvariant()
input.ToUpper().Trim()
input.ToUpper().TrimEnd()
input.ToUpperInvariant().Trim()
input.ToUpperInvariant().TrimEnd()
input.Trim().ToUpper()
input.Trim().ToUpperInvariant()
input.TrimEnd().ToUpperInvariant()
input.TrimEnd().ToUpper() // + lots more solutions

Я не стал выкладывать все решения, их достаточно много. Как видите, все варианты являются более-менее правильными, но не учтена коммутативность вызовов: ведь по сути не важно, вызывать нам .Trim().ToUpper() или .ToUpper().Trim(), но программа этого не знает.

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

ff5

Мы пока что обсуждали только «няшные» функции которые можно вызывать без аргументов. Всё – такое дело больше не прокатит. Чтобы удалить bbb на конце нужно вызвать что-то, что жестко выпиливает или b или bbb или удаляет 3 последние буквы в тексте.

Естественно, что все аргументы вызова должны как-то коррелировать с объектом, на котором идет вызов. Для этого сделан страшный и ужасный FragmentationEngine – класс-дробитель, который умеет дробить другие типы на составные части. (Тут должна быть картинка Дробителя из Hearthstone.)

Давайте возьмем строку aaabbb. Ее можно раздробить так:

  • Все возмоджные буквы (в данном случае – 'a' и 'b')

  • Все возможные подстроки (в т.ч. пустая строка). Это реально болезненная операция, т.к. на длинной строке их очень много.

  • Все возможные числа в пределах длины самой строки. Это нужно для вызовов всяких Substring().

Надробив строку на всякие объекты, мы ищем методы – статические или нет – которые берут эти объекты. Тут все более менее предсказуемо, за иключением того что

  • Вызовы с 2+ аргументами делают нехилый комбинаторный взрыв. Простой пример – это Substring().

  • Вызовы функций которые берут params[] теоретически создают ничем не ограниченный комбинаторный взрыв, поэтому их нужно или лимитировать или не вызывать вообще.

CallSharp, конечно, справляется с нашим синтетическим примером и выдает нам

input.Trim('b')
input.TrimEnd('b')

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

ff6

Хмм, казалось бы, нужно всего лишь удалить er ну или e и r по отдельности. Если запустить CallSharp на этом примере, мы получим

input.Trim('e','r')
input.Trim('r','e')
input.Trim('a','e','r')
input.Trim('a','r','e')
input.Trim('e','a','r')
input.Trim('e','r','a')
input.Trim('r','a','e')
input.Trim('r','e','a')
input.TrimEnd('e','r')
input.TrimEnd('r','e') 
// 30+ more options

Как видите, первые два варианта – единственные, которые хотелось бы использовать. Все остальные обладают излишней информацией, которая не делает никому погоды. Или вот еще

ff7

Тут вариантов меньше, вот они:

input.Replace("aabb", "aa")
input.Replace("bb", "")
input.Replace("bbcc", "cc")

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

Еще одно интересное наблюдение – это то, что иногда интересные решения кроются на глубине, а не на поверхности. Вот например

ff8

Тут можно просто удалить пробел, но CallSharp дает много вариантов, например

input.Replace(" ", string.Empty)
input.Replace(" b ", "b")
input.Replace("a b ", "ab")
input.Replace(" b c", "bc")
input.Replace("a b c", "abc")
// at greater depth,
string.Concat(input.Split())

Лучшие варианты – первый и, возможно, последний – он хоть и подороже с точки зрения выполнения (наверное, не проверял, интуиция подсказывает), но выглядит элегантно. Это хороший пример того, как программа может вывести то, что человек сразу не увидит.

Резюмируя

Сейчас CallSharp работает, скажем так, небыстро. Проблема в основном в использовании reflection (в частности, MethodInfo.Invoke()) а также в комбинаторных взрывах связанных с глубиной вызовов и количеством аргументов и их вариаций.

Текущие проблемы с перформансом отчасти решатся при переезде от динамического до статического reflection (предполагается сделать всё на T4). Оптимизаций можно делать очень много – я бы например хотел сделать аннотации для разметки «коммутативности» как наборов функций, так и аргументов в функциях (например, порядок букв в Trim() не важен).

CallSharp – open source проект, лежит на GitHub. Там же есть его релизы – по ссылке click here установится ClickOnce дистрибутив, которые самообновляется по мере выхода новых версий.

Для тех, кому хочется чуть более живого повествования, ниже представлен мой доклад на Петербургской .NET User Group:

Спасибо за внимание!

Written by Dmitri

17 декабря 2016 at 14:34

Опубликовано в .NET

Tagged with ,

Ключевые особенности FPGA

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

Многие из вас наверное видели, что Amazon запускает инстансы EC2 с FPGA на борту. Я сразу признаюсь, что я никогда не пользовался облаками Amazon: я использую только Azure, и то только потому что мне некоторый объем вычислительным мощностей дают бесплатно как MVP (которым я являюсь уже ажъ 8 лет!), а до этого у меня просто стоял свой собственный сервер в универе, про который мало кто знал пока через одно веб-приложение (MindTouch Core, если кому интересно) на сервере кто-то установил спамбота, и понеслась.

Короче, я не юзаю Амазон облако, да и в последнее время стараюсь поменьше заказывать с Амазона: сейчас ведь там продают очень многие ритейлеры, у которых есть свои сайты, соответственно я скорее куплю за ту же стоимость у них, чтобы им перепало побольше, а Амазону — нуль. Думаю причины сего поведения с моей стороны вы итак прекрасно понимаете — Амазон прекрасен для покупателя (хотя скорость доставки в Англии упала до неприличия), но вот к программистам они, судя по “среднему по больнице”, относятся как к шлаку. И да, понятно что есть продуктовые команды в которых все нормально, но судя по тому что пишут на Reddit, общее положение все же весьма бредовое. Если вы там работаете, можете меня поправить и рассказать как все шикарно.

Ах, да, тьфу ты, я на самом деле немного некорректно тут написал. Основная проблема моя с Амазоном не в этом, а в том что они не берут PayPal. Это как бы критично т.к. на мелкие покупки я трачу только свой disposable income, а он у меня весь на PayPal.

Че-то я отъехал от темы. Вроде пост был про FPGA.

Концепция dataflow

У нас есть много разных плохо коммутируемых парадигм разработки – например процедурная, объектно-ориентированная, функциональная. А есть парадигма, которую можно называть “потоковой”, а по-английски она называется словом dataflow.

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

int sum(int a, int b, int c)
{
  return a + b + c;
}

Возникает вопрос: сколько времени занимает код выше? Если мыслить о том что написано выше в терминах C++, C#, Java или аналогичных языков, то код — то есть набор инструкций — будет транслирован в несколько вызовов add, и соответствено займет ненулевое время на выполнение.

Сколько займет тот же самый алгоритм на FPGA? Ну, если утрировать до боли, но он займет нуль времени. Сигналы a, b и c будут поданы на соответствующие схемы, который в момент на выходе выдадут результат. Никаких “инструкций” не произойдет.

Понимание того, что программрование FPGA — это конфигурирование интегральной микросхемы (я тут возможно путаю терминологию, поправьте если что), а не описание набора инструкций — это ключ к пониманию того, что собственно можно выудить из технологии FPGA и к каким она задачам применима. Сейчас мы как бы понимаем, что когда по проводам идут данные с бешеной скоростью (например, интернет на 10G), обычный CPU — даже самый навороченный Xeon — не сможет эти данные переварить содержательно, разбирая, например, коммуникационный протокол по которому идут биржевые данные. Но это только часть задачи.

Аппаратный параллелизм

Современный процессор, безусловно, поддерживает некоторый уровень параллелизма: у нас есть “многоядерность”, у нас есть т.к. hyper-threading, ну и конечно у нас есть SIMD, который помошает делать больше вычислений за счет больших регистров. Но, так или иначе, этот паралеллизм заранее лимитирован процессором: мы знаем, например, что на Xeon Phi (60 ядер по 4 аппаратных потока каждое) не имеет особого смысла запускать более 240 потоков и, более того, на обычных CPU мы не контролируем, какая задача ложится на какой поток: это обычно делает операционная система (в случае с Xeon Phi там используется свой собственный Linux).

Ситуация с FPGA несколько другая: там за один такт можно делать совершенно несвязанные операции, и количество таких операций лимитировано только количеством логических элементов на кристалле. Иначе говоря, FPGA способствуют такому масштабу параллелизма, о котором с обычными CPU остается только мечтать.

Это не значит, что FPGA дает самый лучший performance. У нас есть очень мощные модели параллелизма (например SIMT на GPU) с которыми FPGA не может бодаться в плане обработки больших объемов данных. Но и цель у FPGA немножко не такая: ведь на GPU каждый поток должен делать одно и то же (иначе теряется вся эффективность), а FPGA может на разных своих участках делать абсолютно разные вещи. Синхронизация между этими участками — это достаточно сложная задача, конечно, но with great power… ну вы поняли.

Да, еще один аспект, который нужно упомянуть — это тактовая частота. На CPU тактовая частота одна, и все задачи синхронизуются на нее. На FPGA вы можете использовать разные генераторы (по английски clock), т.е. сигналы разной частоты для разных задач. Тактовая частота FPGA в целом существенно уступает CPU, но сравнивать их напрямую не особенно интересно, т.к. они служат разным целям.

Концепция pipelining

Я не буду скрывать, что и на обычных Intel’евских CPU происходит много всякой магии вроде branch prediction. По сути, современный ассемблерный код, который выдает вам С++ компилятор с включенными оптимизациями, читать достаточно сложно про причине того, что количество “магии”, которое обычно вкладывается в погони за перформансом, огромно. Ассемблер можно читать разве что в идеальном мире, без оптимизаций.

Pipelining объяснить просто. Возьмем следующий код:

void mad(int* a, int* b, int* c, int* result size_t count)
{
  for (size_t i = 0; i < count; ++i)
    result[i] = a[i]*b[i] + c[i];
}

Вы наверное думаете что каждая итерация цикла for должна закончиться прежде, чем начнется новая. В контексте С++ вы правы, а в контексте FPGA — нет!

Представьте операцию a*b+c как микросхему, работающую под определенной тактовой частотой: на первый шаг, вы подаеет значения a и b и получаете их произведение. Потом надо бы прибавить c, а что в это время делает та часть которая умонжает. Думаете она простаивает и ждет пока завершится вычисление? А вот и нет! Эта часть схемы может брать и считать следующее произведение, т.е. a*b для следующей пары чисел a и b.

Вот это и называется pipelining: возможность сразу пропускать целый поток значений, не дожидаясь завершения всего вычисления. И, как вы надеюсь уже догадались, это приводить к существенному ускорению, т.к. является de facto еще одним уровнем аппаратного параллелизма.

Что такое SoC

SoC расшифровывется как System-on-a-Chip, и в контекте она подразумевает некую аггломерацию FPGA с обычными процессорами вроде ARMов. При этом эти процессоры не просто “сосуществуют на плате”, а ARM встроен прямо в FPGA.

А вот это уже интересно, т.к. на ARM мы просто ставим свою собственную ось (вопрос про то, можно ли туда поставить Windows остается открытым, т.к. MS как-то протормаживают в этом плане), и тем самым получаем на одном кристалле, по сути, полноценно-работающий компьютер — нужно только добавить оперативки и периферию (например PCIe, Ethernet, …) по вкусу. Собственно это должно объяснять почему это “system on a chip” — на одном кристалле все, что нужно чтобы выполнять какую-то экспертную задачу.

Покупка Intel’ем компании Altera, второго по размеру производителя FPGA, намекает на симметричное развитие Xeon’ов: идея в том чтобы воткнуть в процессор какой-нть FPGA и дать разработчикам программировать его и осуществлять взаимодействие между CPU и FPGA вместо того чтобы ходить по PCIe шине, как предлагает Amazon.

Кстати, покупка Altera может выйти сильным боком Российским производителям (в т.ч. ВПК и тем кто под санкциям). Причина проста — сейчас, Altera и в частности ее дистрибьютор Terasic — это коррумпированая Тайваньская лавочка, которая вышлет что угодно и куда угодно, в то время как лидер рынка, Xilinx, каждую закупку пропускает через DoD на предмет санкций, dual use и так далее. Если Intel начнет себя вести так же, настанут очень веселые времена. Или вы думаете что в РФ у кого-то есть производственные мощности для импотрозамещения? Ну-ну.

Высокоуровневый синтез

HLS (high-level synthesis) — это не что иное, как кодогенерация VHDL/Verilog из более популярных языков вроде С++ и SystemC. Последний — этот тот же С++, как мне видится, только с некоторым набором конструктов для системного мира (например fixed-point types).

HLS подходов очень много, и объединяет их то, что все они генерируют очень сложную к прочтению и пониманию кашу, которую уже некомфортно читать. Помимо этого, наивно полагать что вы можете взять уже существующий С++ и просто нажать кнопочку “сделать мне хорошо” — вы не можете так просто поменять процедурную парадигму на поточную. Возможность писать что-то новое на С++ дает, разве что, варианты портирования этого “что-то” на x86 и иже с ним, но опять же, непонятно что это дает — разве что тестировать это можно быстрее, да и в Cling-е гонять.

Лично я склонен думать, что у HLS будущее, и что HDL’и должны отмереть за их чрезмерной низкоуровневостью. Но пока что, они — лучший способ описать, что и как должно произходить в системе. ■

Written by Dmitri

11 декабря 2016 at 22:00

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

Tagged with , , ,