elizarov


Блог Романа Елизарова


Previous Entry Share Next Entry
Детали реализации имеют значение или direct ByteBuffer vs heap ByteBuffer
elizarov

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

В предыдущей записи про оптимизацию кода, я сравнивал класс ByteBuffer с int[]. Для этого сравнения я использовал прямой буфер (direct buffer), получаемый вызовом метода allocateDirect. Это тот самый буфер, который позволяет вынести данные за пределы кучи в языке Java. А что будет если заменить его на буфер в куче (heap buffer), получаемый вызовом метода allocate?

Проверить это не сложно. Достаточно заменить в одной строчке кода allocateDirect на allocate и произвести измерения. Результаты одного из прогонов моей простенькой тестовой программы, в сравнении с предыдущими результатами для прямого буфера, выглядят так (в таблице показано время в наносекундах, затрачиваемое в расчете на одну итерацию цикла сложения целых чисел из буфера):

РазмерViaByteBuffer2 (direct)ViaByteBuffer3 (heap)
1 0000.779.61
10 0000.779.60
100 0000.869.55
1 000 0000.919.68
10 000 0000.949.76

Скорость работы упала более чем в 10 раз. Чем это можно объяснить? Объяснение легко и просто увидеть, если посмотреть на детали реализации класса ByteBuffer. Внутри методов allocateDirect и allocate создаются конкретные реализации — DirectByteBuffer и HeapByteBuffer соответственно. Посмотрим на реализацию метода getInt в классе DirectByteBuffer:

    public int getInt(int i) { 
        return getInt(ix(checkIndex(i, (1 << 2)))); 
    } 
 
    private int getInt(long a) { 
        if (unaligned) { 
            int x = unsafe.getInt(a); 
            return (nativeByteOrder ? x : Bits.swap(x)); 
        } 
        return Bits.getInt(a, bigEndian); 
    } 

    private long ix(int i) { 
        return address + (i << 0); 
    } 
 
    // В классе Buffer
    final int checkIndex(int i, int nb) {
        if ((i < 0) || (nb > limit - i)) 
            throw new IndexOutOfBoundsException(); 
        return i; 
    } 

Видим достаточно прямолинейный код, который проверяет границы, вычисляет адрес, и, при естественном порядке байт и возможности платформы работать с целыми числами независимо от выравнивания, достает целое число напрямую через unsafe.getInt — этот вызов является встроенным (intrinsic) в HotSpot и транслируется в соответствующую инструкцию процессора.

А что происходит в классе HeapByteBuffer? Давайте посмотрим:

    public int getInt(int i) { 
        return Bits.getInt(this, ix(checkIndex(i, 4)), bigEndian); 
    } 

    // В классе Bits
    static int getInt(ByteBuffer bb, int bi, boolean bigEndian) { 
        return bigEndian ? getIntB(bb, bi) : getIntL(bb, bi) ; 
    } 

    static int getIntL(ByteBuffer bb, int bi) { 
        return makeInt(bb._get(bi + 3), 
                       bb._get(bi + 2), 
                       bb._get(bi + 1), 
                       bb._get(bi    )); 
    } 
 
    static private int makeInt(byte b3, byte b2, byte b1, byte b0) { 
        return (((b3       ) << 24) | 
                ((b2 & 0xff) << 16) | 
                ((b1 & 0xff) <<  8) | 
                ((b0 & 0xff)      )); 
    } 

    // Снова в классе HeapByteBuffer
    byte _get(int i) {
        return hb[i]; 
    } 

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

Можно ли из этого сделать вывод, что прямой буфер работает быстрей, чем буфер в куче? Конечно нет! Мы всего лишь увидели, что метод getInt в прямом буфере работает быстрей, однако это ничего не говорит о скорости работы других операций. При написания высокопроизводительного кода, важны детали реализации именно тех методов, которые вы используете, и конкретный способ их применения для решения вашей задачи.

UPDATE: А еще имеет значение что и как замеряется. Подробности в заметке "О замерах времени работы кода или знай что замеряешь часть первая"


  • 1
1. Автор http://www.jboss.org/netty
писал, что его org.jboss.netty.buffer.ChannelBuffer

заметно быстрее стандртного (плюс там много сделано для уменьшения копирования - можно сделать буфер из списка буферов итд)

Раз у вас уже тест готов может протестируете?

Для создания подходящих реализаций буферов у него есть большая фабрика
org.jboss.netty.buffer.ChannelBuffers


2. стандартом де-факто (субъективные наблюдений) для замены массива, на что-то почти такое же быстрое, но более функциональное является
http://trove.starlight-systems.com/

понятно, что можно самому сесть и за 5 минут написать, но всё же интересно знать чем люди там занимаются 10 лет.

Может быть тоже использовать в тесте, как раз свежая версия вышла?

1. "Заметное быстрее стандартного" это странное утверждение и вряд ли оно может быть в целом верно. Для какой задачи быстрее? У нас в QDS тоже, например, есть свои быстрые буфера, которые быстро решают задачи стоящие перед нами. Не сомневаюсь, что авторы jboss свои задачи тоже решили если не оптимально, то близко к этому. Если у меня встанет задача, похожая на ту, которую решали в jboss, то я обязательно посмотрю на их реализацию. Спасибо за наводку.

2. Я не ставлю себе целью написать новый массив, а лишь показываю с разных сторон принципы написания высокопроизводительного кода. И делаю это на очень-очень примитивным примерах. Что касается trove, то я не сомневаюсь, например, что TIntArrayList будет работать быстрей чем ArrayList<Integer> и почти так же быстро как int[] — это очевидное следствие из того, что память это новый диск.

* Для какой задачи быстрее?
Для создания сетевых приложений.
Т.е. вместо ByteBuffer и ручной NIO код - ChannelBuffer и netty.

Как писать быстрые сетевые протоколы я тоже как нибудь расскажу. Там действительно потребуются совсем другие буфера. Но сделать универсально быстрое решение не получится. Для сервера приложений типа jboss нужен будет один дизайн. А чтобы держать одновременно тысячи соединений и передавать по ним миллионы котировок в секунду - другой.

Использовал в тесте Trove. Читайте здесь.

  • 1
?

Log in

No account? Create an account