пятница, 6 января 2012 г.

Неявное приведение типов в Groovy

Не так давно я задавал вопрос в Groovy mail-list - есть ли какой-то устойчивый список вещей, которые надо избегать при написании высокогопроизводительного на Groovy. Среди прочих советов, один из главных разработчиков Groovy, Jochen "blackdrag" Theodorou указал, что в общем случае, зачастую использование конкретного типа при объявлении переменной (например, MyType var = ... вместо def var = ...) может ухудшить производительсть из-за накладных расходов на проверку типов и, если нужно, их приведение.


Небольшой эксперимент, который показывают эти накладные расходы даже на Groovy 1.8.3. Перед этим экспериментом будет полезно просмотреть следующую статью - http://groovy.codehaus.org/From+source+code+to+bytecode, где рассказывается про то, как по шагам исходный код на Groovy преобразуется в байткод JVM, а так же почитать какую-нибудь вводную статью в собственно байткод, например эту - http://www.ibm.com/developerworks/ibm/library/it-haggar_bytecode/.


Итак, простейший код:


class NoStrictType {
   void myMethod1() {
     def a = 4.2
   }
}

class StrictType {
  void myMethod1() {
    int a = 4.2
  }
}

Единственное отличие - во втором классе тип локальной переменной определен явно.  Скомпилируем оба класса и посмотрим на байткод этих методов.



Для первого случая (без конкретного типа):




































Итак, код вполне очевиден. Опустим первую строчку, это получение массива кешированных CallSite-ов, они напрямую к операциям над типами не относятся. Далее мы создаем новый объект типа BigDecimal (все помнят, что по умолчанию в Groovy все нецелые числа представляются как BidDecimal?), дублируем текущее значение на вершине стека операндов, достаем значение 4.2 из пула констант, инициализируем объект BigDecimal, сохраняем ссылку на этот созданный объект во второй ячейке массива локальных переменных текущего фрейма, потом загружаем ее оттуда на вершину стека, и наконец, используем pop;return, чтобы вернуть эту ссылку из метода. Опять же, как все помнят, в Groovy даже без явного оператора return любой метод возвращает последнюю переменную, которая использовалась в вычислениях (точнее, последнюю сссылку, сохраненную на вершине стека операндов на момент окончания работы метода).




Теперь, байткод для второго класса, StrictType.














В чем разница по сравнению с первым случаем? Добавился вызов статического метода DefaultTypeTransformation.intUnbox(). Посмотрим в документацию к этому методу, что он делает.

http://groovy.codehaus.org/api/org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation.html


Видим, что этот метод просто конвертит объект-ссылку в  в объект типа Number, и возвращает примитив.


public static int intUnbox(Object value) {
   Number n = castToNumber(value, int.class);
   return n.intValue();
}
Смотрим, как именно выполняется эта конвертация типов:


public static Number castToNumber(Object object, Class type) {
        if (object instanceof Number)
            return (Number) object;
        if (object instanceof Character) {
            return Integer.valueOf(((Character) object).charValue());
        }
        if (object instanceof GString) {
            String c = ((GString) object).toString();
            if (c.length() == 1) {
                return Integer.valueOf(c.charAt(0));
            } else {
                throw new GroovyCastException(c, type);
            }
        }
        if (object instanceof String) {
            String c = (String) object;
            if (c.length() == 1) {
                return Integer.valueOf(c.charAt(0));
            } else {
                throw new GroovyCastException(c, type);
            }
        }
        throw new GroovyCastException(object, type);
    }
Несколько операторов instanceof, которые не самые дешевые, несколько явных приведений типа, условные операторы, работа с исключениями. Я не мерил, как это влияет на скорость работы в реальных приложениях, но судя по самому факту, сколько дополнительных байткодов надо выполнить в этом случае для приведения типов, впечатляет. Вспомните - сам оригинальный метод - всего 10 байткодов.


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

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

    А в более общем случае в груви компилятор всегда знает конкретный тип справа в момент компиляции?

    ОтветитьУдалить
  2. What the heck, somehow I can't type in in Russian in comments, don't know why.

    Generally - no, the concrete type isn't known in compile type, simply -because of the class may be loaded in runtime and casted down to some known interface. Also, Groovy has powerful type coercion system, which also complicates the case.

    ОтветитьУдалить