Re[7]: Ускорение выполнения задачи
От: Безон Великобритания  
Дата: 20.07.09 20:12
Оценка:
Здравствуйте, Blazkowicz, Вы писали:

B>Здравствуйте, Безон, Вы писали:


B>>>Которые многопоточностью все равно не разрулить, так как они тормозят в любом случае.

Б>>многопоточностью (в число потоков больше чем число ядер) разгоняется только код большую часть времени находящийся в ожидании данных. Точно так же он разгоняется событийно-управляемым кодом
B>Напоминаю что разговор идет о создании отчетов для миллиона клиентов. Есть подозрения что отчеты создаются не одним запросом.
Ну обычно массированных вычислений на клиенте бд не производят.


Б>>>>ЗЫ. переключение контекста не фига не бесплатно, поэтому бездумное распараллеливание легко просаживает производительность.

B>>>На 10 потоках и 2х ядрах можно считать что бесплатное.
Б>>ну ну блажен кто верует ... Советую написать простой тест без ожидания ввода-вывода
B> Написал. Убедился в своей правоте. Доказывать свою точку зрения на сферическом коне в вакууме можешь сколь угодно долго. Что будет следующим аргументом? Пример где в цикле вызывается Thread.yield()?
ну не совсем например очередь через wait notify
-----
Re[7]: Ускорение выполнения задачи
От: Безон Великобритания  
Дата: 21.07.09 04:05
Оценка:
Здравствуйте, Blazkowicz, Вы писали:

B> Написал. Убедился в своей правоте. Доказывать свою точку зрения на сферическом коне в вакууме можешь сколь угодно долго. Что будет следующим аргументом? Пример где в цикле вызывается Thread.yield()?


Ты сначала докажи вот это утверждение:

B> А почему нет? Без узких мест распареллеливание даже на одноядерном процессоре даст видимый прирост.


Пока только видно что прироста это не дает, а есть даже потери.
-----
Re[9]: Ускорение выполнения задачи
От: denis.zhdanov Россия http://denis-zhdanov.blogspot.com/
Дата: 21.07.09 04:30
Оценка:
Здравствуйте, Безон, Вы писали:

Б>Тест замечательный. И действительно показывает, что при полной изоляции данных для алгоритмов, параллельное их выполнение не ведет к сильному просаживанию производительности. Правда немного смущает использование Atomic при гарантированно однопоточном доступе.

Б>Однако когда алгоритмы работают над разделяемыми данными, а тем более их модифицируют, вот тогда производительность упадет значительно больше. Насколько больше сказать не могу пока не будет теста.

Тест и был задуман работать с thread-scoped данными, чтобы издержки от многопоточности выражались только в переключении контекста. Atomic используется только потому, что быстрее написать код, который вызывает incrementAndGet(), чем инкрементить вручную.

Понятно, что при lock contention производительность проседать будет, и сильно, но изначально вызвало спор утверждение, что десять потоков на двух ядрах дадут заметную деградацию производительности из-за переключения контекста.
http://denis-zhdanov.blogspot.com
Re[8]: Ускорение выполнения задачи
От: Blazkowicz Россия  
Дата: 21.07.09 05:08
Оценка: 1 (1)
Здравствуйте, Безон, Вы писали:

B>> А почему нет? Без узких мест распареллеливание даже на одноядерном процессоре даст видимый прирост.

Б>Пока только видно что прироста это не дает, а есть даже потери.
Для тех кто в танке: "В базе есть миллион клиентов, для каждого клиента нужно сгенерировать отчет и сохранить обратно в базу"
Re: Ускорение выполнения задачи
От: Безон Великобритания  
Дата: 21.07.09 18:37
Оценка:
Здравствуйте, Аноним, Вы писали:

А>В базе есть миллион клиентов, для каждого клиента нужно сгенерировать отчет и сохранить обратно в базу.

А>Сейчас на эту задачу уходит 5 часов если выполнять последовательно.
А>Попробовал через пул-потокв (10 потоков) — заняло 4,5 часов.
А>Согласно бизнес требованиям задача должна выполнятся не более 1 часа.
Раз в базе миллион клиентов то объем вероятно достаточно большой?


А>Приложение использует Spring + Hibernate и одну базу данных. Потяно что потоки конкурируют за доступ к данным, но все равно мне непонятно почему 10 потоков не дали значительного прироста производительности.


А>Какие еще могут быть варианты.


Есть вариант что производительность упирается в СУБД (кстати какая она). Я бы рекомендовал использовать шардинг данных (например, разбить таблицы на партиции по компаниям), разделив всю базу на такое количество шард, чтобы каждая целиком помещалась в память (вместе с индексами). После этого в несколько потоков генерировать отчеты по компаниям из одной шарды.
-----
Re[8]: Ускорение выполнения задачи
От: Golovach Ivan Украина http://kharkovitcourses.blogspot.com
Дата: 09.09.09 10:27
Оценка: 26 (3)
Здравствуйте, denis.zhdanov, Вы писали:

DZ>Итого разница составила около двух с половиной процентов, что несколько удвило, т.к. переключение контекста субъективно считалось мною более дорогой операцией. Понятно, что в тесте присутствует достаточно большая погрешность, но тем не менее тенденция налицо.



Само по себе переключение контекста занимает весьма немного времени.
Порядка 2.000-4.000 тактов процессора.

Обоснования:
1)
Вот выдержки из класса java.util.concurrent.Exchanger:

public class Exchanger<V> {
    ...
    private static final int NCPU = Runtime.getRuntime().availableProcessors();
    ....
    /**
     * The number of times to spin (doing nothing except polling a
     * memory location) before blocking or giving up while waiting to
     * be fulfilled.  Should be zero on uniprocessors.  On
     * multiprocessors, this value should be large enough so that two
     * threads exchanging items as fast as possible block only when
     * one of them is stalled (due to GC or preemption), but not much
     * longer, to avoid wasting CPU resources.  Seen differently, this
     * value is a little over half the number of cycles of an average
     * context switch time on most systems.  The value here is
     * approximately the average of those across a range of tested
     * systems.
     */
    private static final int SPINS = (NCPU == 1) ? 0 : 2000;


т.е. авторы java.util.concurrent считают, что на большинстве многоядерных систем переключение контекста лежит в диапазоне 3000-4000 тактов процессора.

2)
Вот выдержки из класса java.util.concurrent.SynchronousQueue:

    /**
     * The number of nanoseconds for which it is faster to spin
     * rather than to use timed park. A rough estimate suffices.
     */
    static final long spinForTimeoutThreshold = 1000L;


Если считать типичной тактовой частотой 2-3ГГц, то выходит, что авторы java.util.concurrent считают, что время, соизмеримое с переключением контекста порядка 2000-3000 тактов

3)
Следственный эксперимент: класс PingPong запускает два класса Player, а они перебрасываются классом Item.
Item содержит простой счетчик типа int, считающий количество передач. Передачи осуществляются через механизм wait()/notify().
На тестовом компьютере (AMD Duron 1300, WinXP, jdk 1.0.6_13) 100.000.000 передач было выполнено за 232с, т.е. на одно переключение контекста уходило в среднем 232 * 10 * 1.3 = 3016 тактов.

Код:


public class Item {
    int counter = 0;
}

public class PingPong {
    // constant
    public static final int ITERATION_COUNT = 100 * 1000 * 1000;
    // global variables :(
    public static final Item item = new Item();
    public static final CountDownLatch start = new CountDownLatch(1);
    public static final CountDownLatch stop = new CountDownLatch(2);

    public static void main(String[] args) throws InterruptedException {
        // init section
        new Thread(new Player(false)).start(); //player #0
        new Thread(new Player(true)).start();  //player #1

        // work section
        long startTime = System.nanoTime();
        start.countDown();
        stop.await();
        long deltaTime = System.nanoTime() - startTime;

        // info-to-console section
        System.out.println("deltaTime: " + deltaTime);
    }
}

public class Player implements Runnable {
    private final boolean imOdd;

    public Player(boolean imOdd) {
        this.imOdd = imOdd;
    }

    public void run() {
        // await for start ping-ponging
        try {
            PingPong.start.await();
        } catch (InterruptedException e) {e.printStackTrace();}

        synchronized (PingPong.item) {
            while (true) {
                // wait for my turn (avoid spirious wakeup)
                while (((PingPong.item.counter & 1) == 1) == imOdd) {
                    try {
                        PingPong.item.wait(); // wait for "Ping"
                    } catch (InterruptedException e) {e.printStackTrace();}
                }

                // increment Ping-Poin counter
                PingPong.item.counter++;

                // do "Pong"
                PingPong.item.notify();

                if (PingPong.item.counter >= PingPong.ITERATION_COUNT) {
                    PingPong.stop.countDown(); // show for PingPoint.class that my work doing
                    return; // break, thats all
                }
            }
        }
    }
}
Re[9]: Ускорение выполнения задачи
От: Golovach Ivan Украина http://kharkovitcourses.blogspot.com
Дата: 09.09.09 16:07
Оценка:
Здравствуйте, Безон, Вы писали:

Б>Тест замечательный. И действительно показывает, что при полной изоляции данных для алгоритмов, параллельное их выполнение не ведет к сильному просаживанию производительности. Правда немного смущает использование Atomic при гарантированно однопоточном доступе.

Б>Однако когда алгоритмы работают над разделяемыми данными, а тем более их модифицируют, вот тогда производительность упадет значительно больше. Насколько больше сказать не могу пока не будет теста.

При полной изоляции данных — каждый поток будет обновлять кэш данных процессора (а возможно и кэш кода), а это процедура более затратная чем переключение контекста. Как раз, полагаю, желательно для массивных по данным вычислений (обработка аудио, видео, фото, работа с большими текстами) желательно иметь максимальное кол-во общих данных на чтение. Для записи использовать какие-то неблокирующие разделяемые структуры (так понимаю обращение к атомарным переменным занимает порядка 100-200циклов). Т.е., например, для декодирования JPG в массив цветов, думаю разумнее не делить одному потоку первые 1000 квадратов, второму — вторую тысячу, а как раз динамически выделять каждому по запросу, скажем, по 10 квадратов подряд.

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

Следственный эксперимент: класс PingPong запускает два класса Player, а они перебрасываются классом Item.
Item содержит простой счетчик типа int, считающий количество передач. Передачи осуществляются через механизм wait()/notify().
Кроме того, каждый поток пробегает по одному из массивов (считывает каждый 16-ый елемент для подгрузки массива в кэш) (четный поток читает cache_1, нечетный — cache_0).

На тестовом компьютере (AMD Duron 1300(L1_cache size=64K+64K(code+data), L2_cache size=64K), WinXP, jdk 1.0.6_13) размер кэша первого уровня для данных 64К.
Эксперимент показывает, что оптимальная производительность достигается в том случае, когда каждый поток использует массивы интов по 8192(т.е. 2 массива по 32Кб) (deltaTime/cacheSize: 779640) и производительность в 7.24 раза хуже при использовании массивово интов по 16384(т.е. 2 массива по 64Кб) (deltaTime/cacheSize: 5643590).
Во втором случае каждый из потоков полностью загружает кэш данных первого уровня своими данными выталкивая данные другого.
// в коде все размеры в интах, т.е. размеры в байтах в 4 раза больше

Результаты:
cacheSize: 2048 deltaTime: 3064966357 deltaTime/cacheSize: 1496565
cacheSize: 3072 deltaTime: 3460753253 deltaTime/cacheSize: 1126547
cacheSize: 4096 deltaTime: 4406545804 deltaTime/cacheSize: 1075816
cacheSize: 5120 deltaTime: 5075649838 deltaTime/cacheSize: 991337
cacheSize: 6144 deltaTime: 5553419778 deltaTime/cacheSize: 903876
cacheSize: 7168 deltaTime: 5848771562 deltaTime/cacheSize: 815955
cacheSize: 8192 deltaTime: 6386814526 deltaTime/cacheSize: 779640
cacheSize: 9216 deltaTime: 8158033214 deltaTime/cacheSize: 885203
cacheSize: 10240 deltaTime: 9952118216 deltaTime/cacheSize: 971886
cacheSize: 11264 deltaTime: 11804108903 deltaTime/cacheSize: 1047950
cacheSize: 12288 deltaTime: 14448519295 deltaTime/cacheSize: 1175823
cacheSize: 13312 deltaTime: 17866376034 deltaTime/cacheSize: 1342125
cacheSize: 14336 deltaTime: 28771726853 deltaTime/cacheSize: 2006956
cacheSize: 15360 deltaTime: 52159304604 deltaTime/cacheSize: 3395788
cacheSize: 16384 deltaTime: 92464582510 deltaTime/cacheSize: 5643590

Дополнительный эксперимент. Просто так, под рукой был код и я решил продемонстрировать как выглядят размеры кэшей с точки зрения явиста :
Пусть оба потока работают с одним массивом и покажем что в тот момент когда массив перестает влезать в кэш происходит катастрофическое падение производительности.
Модифицируем строки
PingPong.java:
public static final int ARRAY_LENGTH = 16 * 1024;
заменим на
public static final int ARRAY_LENGTH = 32 * 1024;

for (int cacheSize = 2 * 1024; cacheSize <= 16 * 1024; cacheSize += 1 * 1024) {
заменим на
for (int cacheSize = 2 * 1024; cacheSize <= 32 * 1024; cacheSize += 1 * 1024) {

Player.java:
int[] cache = imOdd ? PingPong.cache_0 : PingPong.cache_1; // different players choice different arrays
заменим на
int[] cache = PingPong.cache_0;

Результаты:
cacheSize: 2048 deltaTime: 3072664821 deltaTime/cacheSize: 1500324
cacheSize: 3072 deltaTime: 3449649327 deltaTime/cacheSize: 1122932
cacheSize: 4096 deltaTime: 3889106856 deltaTime/cacheSize: 949488
cacheSize: 5120 deltaTime: 4214458821 deltaTime/cacheSize: 823136
cacheSize: 6144 deltaTime: 4461290751 deltaTime/cacheSize: 726121
cacheSize: 7168 deltaTime: 4962198827 deltaTime/cacheSize: 692271
cacheSize: 8192 deltaTime: 5531942747 deltaTime/cacheSize: 675285
cacheSize: 9216 deltaTime: 6190682336 deltaTime/cacheSize: 671732
cacheSize: 10240 deltaTime: 6941616882 deltaTime/cacheSize: 677892
cacheSize: 11264 deltaTime: 7272263374 deltaTime/cacheSize: 645619
cacheSize: 12288 deltaTime: 7696612279 deltaTime/cacheSize: 626351
cacheSize: 13312 deltaTime: 8569552275 deltaTime/cacheSize: 643746
cacheSize: 14336 deltaTime: 9151158038 deltaTime/cacheSize: 638334
cacheSize: 15360 deltaTime: 10124464423 deltaTime/cacheSize: 659144
cacheSize: 16384 deltaTime: 10504098146 deltaTime/cacheSize: 641119
cacheSize: 17408 deltaTime: 12899692838 deltaTime/cacheSize: 741020
cacheSize: 18432 deltaTime: 15345301987 deltaTime/cacheSize: 832535
cacheSize: 19456 deltaTime: 17966401317 deltaTime/cacheSize: 923437
cacheSize: 20480 deltaTime: 20462613265 deltaTime/cacheSize: 999151
cacheSize: 21504 deltaTime: 22766556922 deltaTime/cacheSize: 1058712
cacheSize: 22528 deltaTime: 25043635714 deltaTime/cacheSize: 1111667
cacheSize: 23552 deltaTime: 27430734074 deltaTime/cacheSize: 1164688
cacheSize: 24576 deltaTime: 33005879518 deltaTime/cacheSize: 1343012
cacheSize: 25600 deltaTime: 34585078551 deltaTime/cacheSize: 1350979
cacheSize: 26624 deltaTime: 37485102715 deltaTime/cacheSize: 1407944
cacheSize: 27648 deltaTime: 43297514070 deltaTime/cacheSize: 1566026
cacheSize: 28672 deltaTime: 53516617894 deltaTime/cacheSize: 1866511
cacheSize: 29696 deltaTime: 85726250403 deltaTime/cacheSize: 2886794
cacheSize: 30720 deltaTime: 115445316653 deltaTime/cacheSize: 3757985
cacheSize: 31744 deltaTime: 148583765255 deltaTime/cacheSize: 4680688
cacheSize: 32768 deltaTime: 184068212581 deltaTime/cacheSize: 5617316

Видно, что при размере массива 16384(64К) производительность(641119) близка к оптимальной(626351), а при при размере массива 32768(128К) производительность(5617316) ухудшилась в 8.76 раз.
При чем ухудшение началось ровно в тот момент, как массив перестал помещаться в кэш (переход от 16384 елементов к 17408 елементов и ухудшение производительности 1.16 раза).

Исходный код:
public class Item {
    int counter = 0;
}


public class PingPong {
    // constant
    public static final int ITERATION_COUNT = 1000000;
    public static final int ARRAY_LENGTH = 16 * 1024;
    public static final int CACHE_INDEX_STEP = 16;
    // global variables :(
    public static int[] cache_0 = new int[ARRAY_LENGTH]; // 64K (size(int) = 4)
    public static int[] cache_1 = new int[ARRAY_LENGTH]; // 64K (size(int) = 4)
    public static Item item;
    public static CountDownLatch start;
    public static CountDownLatch stop;

    public static void main(String[] args) throws InterruptedException {

        for (int cacheSize = 2 * 1024; cacheSize <= 16 * 1024; cacheSize += 1 * 1024) {
            // init section
            item = new Item();
            start = new CountDownLatch(1);
            stop = new CountDownLatch(2);
            new Thread(new Player(cacheSize, false)).start();
            new Thread(new Player(cacheSize, true)).start();

            // work section
            long startTime = System.nanoTime();
            start.countDown();
            stop.await();
            long deltaTime = System.nanoTime() - startTime;

            // info-to-console section
            System.out.println("cacheSize: " + cacheSize + " deltaTime: " + deltaTime + " deltaTime/cacheSize: " + deltaTime/cacheSize);
        }
    }
}


public class Player implements Runnable {
    private final int cacheSize;
    private final boolean imOdd;

    public Player(int cacheSize, boolean imOdd) {
        this.cacheSize = cacheSize;
        this.imOdd = imOdd;
    }

    public void run() {
        // await for start ping-ponging
        try {
            PingPong.start.await();
        } catch (InterruptedException e) {e.printStackTrace();}

        synchronized (PingPong.item) {
            while (true) {
                // wait for my turn (avoid spirious wakeup)
                while (((PingPong.item.counter & 1) == 1) == imOdd) {
                    try {
                        PingPong.item.wait(); // wait for "Ping"
                    } catch (InterruptedException e) {e.printStackTrace();}
                }

                // increment Ping-Poin counter
                PingPong.item.counter++;

                // read array elements for loading caches
                int sum = 0;
                int[] cache = imOdd ? PingPong.cache_0 : PingPong.cache_1; // different players choice different arrays
                for (int indx = 0; indx < cacheSize; indx += PingPong.CACHE_INDEX_STEP) {
                    sum += cache[indx];
                }

                // do "Pong"
                PingPong.item.notify();

                if (PingPong.item.counter >= PingPong.ITERATION_COUNT) {
                    PingPong.stop.countDown(); // show for PingPoint.class that my work doing
                    return; // break, thats all
                }
            }
        }
    }
}
Re: Ускорение выполнения задачи
От: Infernal Россия  
Дата: 10.09.09 16:21
Оценка:
Здравствуйте, Аноним, Вы писали:

А>Какие еще могут быть варианты.

На текущем проекте столкнулись с системой построения отчетов, которой мы предоставляем данные, а она по ним генерирует всякие разные отчеты. В результате дискуссий с ними выяснилось, что они раз в сутки забирают у нас данные за день из плоских таблиц, они над ними производят разные трансформации в другие более простые таблицы, часто специфические для каждого отчета. А только потом по этим данным быстро генерят конечные отчеты.
Так конечно дикие кластера, но возможно стоит так же копнуть в сторону промежуточных трансформаций и упращение данных.
Re: Ускорение выполнения задачи
От: Аноним  
Дата: 15.09.09 07:24
Оценка:
Здравствуйте, Аноним, Вы писали:

А>В базе есть миллион клиентов, для каждого клиента нужно сгенерировать отчет и сохранить обратно в базу.

А>Сейчас на эту задачу уходит 5 часов если выполнять последовательно.
А>Попробовал через пул-потокв (10 потоков) — заняло 4,5 часов.
А>Согласно бизнес требованиям задача должна выполнятся не более 1 часа.

Попробуйте реализовать эту логику в виде хранимой процедуры(stored procedure). Сделайте хотя бы прототип. + Посмотрите планы запросов. Разберитесь с индексами.
Если у вас есть выделенный DBA, обратитесь к нему.
Понимаю, что business logic на Java- это прогрессивнее, чем корявые языки PL/SQL (Oracle) или T-SQL (MS SQLServer).
Но иногда без них никак не обойтись. Кстати сейчас и MySQL поддерживает хранимые процедуры.
Re: Ускорение выполнения задачи
От: Аноним  
Дата: 15.09.09 07:34
Оценка:
Здравствуйте, Аноним, Вы писали:

А>В базе есть миллион клиентов, для каждого клиента нужно сгенерировать отчет и сохранить обратно в базу.

А>Сейчас на эту задачу уходит 5 часов если выполнять последовательно.
А>Попробовал через пул-потокв (10 потоков) — заняло 4,5 часов.
А>Согласно бизнес требованиям задача должна выполнятся не более 1 часа.

А>Какие еще могут быть варианты.


Попробуйте загрузить данные из базы в память вашего Java процесса и запроцессить эти данные без обращения к БД.
Миллион записей — не ахти сколько. Главное — дайте Java побольше памяти.
Проверьте скорость обмена между компом, где живет сервер БД, и компом, где запускается Java программа. Если канал узкий, то переместите либо сервер БД и Java программу поближе друг к другу.
Re: Ускорение выполнения задачи
От: Baudolino  
Дата: 15.09.09 10:36
Оценка:
В часе 3.6 млн миллисекунд, т.е. вам надо тратить на один отчет в среднем 3.6 миллисекунды. Чтобы уложиться в это время нужно ооочень сильно постараться.

1. Выпилите из проекта Hibernate, оптимизируйте вручную JDBC-запросы, словари закешируйте.
2. Измените порядок операций. Если возможно, читайте сразу большие объемы данных в память в структуры, более подходящие для предполагаемого алгоритма доступа к ним. Добавляйте отчеты в базу пачками.
3. (Альтернатива п.2) Предвычисления. Не всегда возможны, но подумать об этом стоит, т.к. могут очень сильно экономить время.
4. Опытным путём подберите оптимальное число потоков выполнения (не фиксируйте это число — на другом железе может быть оптимальной другая цифра).
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.