Какой язык я мог бы использовать для быстрого выполнения этой задачи суммирования базы данных?

Поэтому я написал программу на Python для выполнения небольшой задачи по обработке данных.

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

parse "%s %lf %s" aa bb cc | group_by aa | quickselect --key=bb 0:5 | \
    flatten | format "%s %lf %s" aa bb cc

То есть для каждой строки разберите слово, число с плавающей запятой и еще одно слово. Думайте о них как об идентификаторе игрока, счете и дате. Мне нужны пять лучших результатов и даты для каждого игрока. Размер данных не тривиален, но и не огромен; около 630 мегабайт.

Я хочу знать, на каком реальном исполняемом языке я должен был написать его, чтобы он был таким же коротким (как Python ниже), но намного быстрее.

#!/usr/bin/python
# -*- coding: utf-8; -*-
import sys

top_5 = {}

for line in sys.stdin:
    aa, bb, cc = line.split()

    # We want the top 5 for each distinct value of aa.  There are
    # hundreds of thousands of values of aa.
    bb = float(bb)
    if aa not in top_5: top_5[aa] = []
    current = top_5[aa]
    current.append((bb, cc))

    # Every once in a while, we drop the values that are not in
    # the top 5, to keep our memory footprint down, because some
    # values of aa have thousands of (bb, cc) pairs.
    if len(current) > 10:
        current.sort()
        current[:-5] = []

for aa in top_5:
    current = top_5[aa]
    current.sort()
    for bb, cc in current[-5:]:
        print aa, bb, cc

Вот некоторые примеры входных данных:

3 1.5 a
3 1.6 b
3 0.8 c
3 0.9 d
4 1.2 q
3 1.5 e
3 1.8 f
3 1.9 g

Вот результат, который я получаю от него:

3 1.5 a
3 1.5 e
3 1.6 b
3 1.8 f
3 1.9 g
4 1.2 q

Существует семь значений для 3, поэтому мы опускаем значения c и d, потому что их значение bb выводит их из первой пятерки. Поскольку 4 имеет только одно значение, его «пятерка лучших» состоит только из этого одного значения.

Это работает быстрее, чем выполнение тех же запросов в MySQL (по крайней мере, таким способом, который мы нашли для выполнения запросов), но я почти уверен, что большую часть времени он проводит в интерпретаторе байт-кода Python. Я думаю, что на другом языке я, вероятно, мог бы заставить его обрабатывать сотни тысяч строк в секунду, а не в минуту. Поэтому я хотел бы написать его на языке с более быстрой реализацией.

Но я не уверен, какой язык выбрать.

Я не смог понять, как выразить это в виде одного запроса на SQL, и на самом деле меня совершенно не впечатляют возможности MySQL даже просто select * from foo into outfile 'bar'; входных данных.

C — очевидный выбор, но такие вещи, как line.split(), сортировка списка из двух кортежей и создание хеш-таблицы требуют написания некоторого кода, которого нет в стандартной библиотеке, поэтому в итоге я получил бы 100 или более строк кода вместо 14. .

Кажется, что C++ может быть лучшим выбором (в стандартной библиотеке есть строки, карты, пары и векторы), но кажется, что код с STL будет намного более запутанным.

OCaml подойдет, но есть ли у него эквивалент line.split(), и не буду ли я грустить по поводу производительности его карты?

Common Lisp может работать?

Есть ли какой-то эквивалент Matlab для вычислений в базе данных, который позволяет мне переводить циклы в быстрый код? Кто-нибудь пробовал Свинья?

(Редактировать: ответил на комментарий davethegr8, предоставив несколько примеров входных и выходных данных, и исправил ошибку в программе Python!)

(Дополнительное редактирование: Вау, эта ветка комментариев действительно превосходна. Всем спасибо!)

Редактировать:

Был ужасно похожий вопрос задавали на sbcl-devel в 2007 (спасибо, Райнер!), и вот awk скрипт от Уилла Хартунга для создания некоторого теста данные (хотя у него нет Zipfian распределения реальных данных):

BEGIN {
 for (i = 0; i < 27000000; i++) {
  v = rand();
  k = int(rand() * 100);
  print k " " v " " i;
 }
 exit;
}

person Kragen Javier Sitaker    schedule 23.09.2009    source источник
comment
Некоторые примеры данных и желаемый результат могут помочь в вашем вопросе. Это довольно неоднозначно. Кроме того, операторы CREATE TABLE... примеры данных в SQL...   -  person davethegr8    schedule 23.09.2009
comment
У меня нет предложения по языку, но одна вещь, которая может помочь, - это отследить минимальное значение для каждого аа, необходимое для его размещения в первой пятерке в отдельной таблице/словаре. Это потенциально позволит вам пропустить большую часть сортировки с очень небольшими затратами.   -  person Pillsy    schedule 23.09.2009
comment
Предполагая, что у нас есть большой файл для начала (то есть исключая SQL ради аргумента), я заметил, что большинство решений являются однопоточными. Интересным аспектом является использование нескольких потоков. Однако есть две проблемы: (а) программа может быть не «короткой», как требуется, и (б) она может быть не быстрее. Прошлой ночью я потратил много времени на многопоточное Java-решение, и, увы, оно не быстрее... пока ;-)   -  person Michael Easter    schedule 26.09.2009
comment
Майкл: Я думаю, что решения Pig и Matlab могут с пользой использовать многопоточность. Так случилось, что все фактические решения на пару порядков хуже, чем нижняя граница, установленная моим фильтром C, поэтому я сначала сосредоточился на однопоточной производительности, но производственная цель на данный момент 16-ядерная коробка...   -  person Kragen Javier Sitaker    schedule 26.09.2009


Ответы (18)


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

Помимо времени, затрачиваемого на синтаксический анализ ввода, скрипт должен «продолжать» сортировку по массиву и т. д.

Ниже приведено первое предположение о том, что должно работать прилично быстро в SQL, предполагая индекс (*) для столбцов таблицы aa, bb, cc в указанном порядке. (Возможной альтернативой может быть индекс «aa, bb DESC, cc».

(*) Этот индекс может быть кластеризованным или нет, что не влияет на следующий запрос. Выбор кластеризации или нет, а также необходимость отдельного индекса «aa, bb, cc» зависит от варианта использования, от размера строк в таблице и т. д. и т. д.

SELECT T1.aa, T1.bb, T1.cc , COUNT(*)
FROM tblAbc T1
LEFT OUTER JOIN tblAbc T2 ON T1.aa = T2.aa AND 
         (T1.bb < T2.bb OR(T1.bb = T2.bb AND T1.cc < T2.cc))
GROUP BY T1.aa, T1.bb, T1.cc
HAVING COUNT(*) < 5  -- trick, remember COUNT(*) goes 1,1,2,3,...
ORDER BY T1.aa, T1.bb, T1.cc, COUNT(*) DESC

Идея состоит в том, чтобы подсчитать, сколько записей в пределах заданного значения aa меньше, чем self. Однако есть небольшая хитрость: нам нужно использовать соединение LEFT OUTER, чтобы не отбросить запись с наибольшим значением bb или последнюю (которая может оказаться одной из первых 5). В результате присоединения к нему слева значение COUNT(*) составляет 1, 1, 2, 3, 4 и т. д., и, следовательно, тест HAVING равен «‹5», чтобы эффективно выбрать первые 5.

Чтобы эмулировать образец вывода OP, ORDER BY использует DESC в COUNT(), который можно удалить, чтобы получить более традиционный тип списка 5 лучших. Кроме того, при желании COUNT() в списке выбора можно удалить, это не влияет на логику запроса и возможность правильной сортировки.

Также обратите внимание, что этот запрос является детерминированным с точки зрения работы со связями, т. е. когда заданный набор записей имеет одно и то же значение для bb (внутри группы aa); Я думаю, что программа на Python может выдавать несколько иные результаты при изменении порядка входных данных из-за случайного усечения словаря сортировки.

Реальное решение: процедурный подход на основе SQL

Описанный выше подход с самосоединением демонстрирует, как можно использовать декларативные операторы для выражения требования OP. Однако этот подход наивен в том смысле, что его производительность грубо связана с суммой квадратов количества записей в каждой «категории». (не O (n ^ 2), а примерно O ((n/a) ^ 2), где a — количество различных значений для столбца aa). Другими словами, он хорошо работает с такими данными, что в среднем количество связанных записей с заданным значением aa не превосходит нескольких десятков. Если данные таковы, что столбец aa не является выборочным, следующий подход подходит намного — намного! — лучше. Он использует эффективную структуру сортировки SQL, реализуя при этом простой алгоритм, который трудно выразить в декларативной форме. Этот подход можно дополнительно улучшить для наборов данных с особенно огромным количеством записей в каждой/большинстве «категорий» aa путем введения простого бинарного поиска следующего значения aa путем просмотра вперед (а иногда и назад...) в курсоре. Для случаев, когда количество «категорий» aa по отношению к общему количеству строк в tblAbc невелико, см. еще один подход после следующего.

DECLARE @aa AS VARCHAR(10), @bb AS INT, @cc AS VARCHAR(10)
DECLARE @curAa AS VARCHAR(10)
DECLARE @Ctr AS INT

DROP TABLE  tblResults;
CREATE TABLE tblResults
(  aa VARCHAR(10),
   bb INT,
   cc VARCHAR(10)
);

DECLARE abcCursor CURSOR 
  FOR SELECT aa, bb, cc
  FROM tblABC
  ORDER BY aa, bb DESC, cc
  FOR READ ONLY;

OPEN abcCursor;

SET @curAa = ''

FETCH NEXT FROM abcCursor INTO @aa, @bb, @cc;
WHILE @@FETCH_STATUS = 0
BEGIN
    IF @curAa <> @aa
    BEGIN
       SET @Ctr = 0
       SET @curAa = @aa
    END
    IF @Ctr < 5
    BEGIN
       SET @Ctr = @Ctr + 1;
       INSERT tblResults VALUES(@aa, @bb, @cc);
    END
    FETCH NEXT FROM AbcCursor INTO @aa, @bb, @cc;
END;

CLOSE abcCursor;
DEALLOCATE abcCursor;

SELECT * from tblResults
ORDER BY aa, bb, cc    -- OR .. bb DESC ... for a more traditional order.

Альтернатива вышеприведенному для случаев, когда aa очень неселективный. Другими словами, когда у нас относительно мало «категорий». Идея состоит в том, чтобы пройтись по списку отдельных категорий и выполнить запрос «LIMIT» (MySql) «TOP» (MSSQL) для каждого из этих значений. Для справки: следующее выполняется за 63 секунды для tblAbc из 61 миллиона записей, разделенных на 45 значений aa, в MSSQL 8.0 на относительно старом/слабом хосте.

DECLARE @aa AS VARCHAR(10)
DECLARE @aaCount INT

DROP TABLE  tblResults;
CREATE TABLE tblResults
(  aa VARCHAR(10),
   bb INT,
   cc VARCHAR(10)
);

DECLARE aaCountCursor CURSOR 
  FOR SELECT aa, COUNT(*)
  FROM tblABC
  GROUP BY aa
  ORDER BY aa
  FOR READ ONLY;
OPEN aaCountCursor;


FETCH NEXT FROM aaCountCursor INTO @aa, @aaCount
WHILE @@FETCH_STATUS = 0
BEGIN
    INSERT tblResults 
       SELECT TOP 5 aa, bb, cc
       FROM tblproh
       WHERE aa = @aa
       ORDER BY aa, bb DESC, cc

    FETCH NEXT FROM aaCountCursor INTO @aa, @aaCount;
END;

CLOSE aaCountCursor
DEALLOCATE aaCountCursor

SELECT * from tblResults
ORDER BY aa, bb, cc    -- OR .. bb DESC ... for a more traditional order.

К вопросу о том, нужен индекс или нет. (см. примечание OP) При простом запуске «SELECT * FROM myTable» сканирование таблицы является фактически самым быстрым подходом, не нужно беспокоиться об индексах. Однако основная причина, по которой SQL обычно лучше подходит для такого рода вещей (помимо того, что он является репозиторием, в котором данные накапливаются в первую очередь, тогда как любое внешнее решение должно учитывать время для экспорта соответствующих данных), заключается в том, что он может полагаться на индексы, чтобы избежать сканирования. Многие языки общего назначения гораздо лучше подходят для обработки необработанных данных, но они ведут нечестную борьбу с SQL, потому что им необходимо восстановить все предыдущие знания о данных, которые SQL собрал в ходе фазы сбора/импорта данных. Поскольку сортировка обычно требует много времени, а иногда и места, SQL и его относительно медленная вычислительная мощность часто оказываются впереди альтернативных решений.

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

Итак... SQL лучше?

При этом, если мы пытаемся сравнить SQL и другие языки для чистых заданий ETL, т. е. для работы с кучами (неиндексированными таблицами) в качестве входных данных для выполнения различных преобразований и фильтрации, вполне вероятно, что мульти- многопоточные утилиты, написанные, скажем, на C и использующие эффективные библиотеки сортировки, вероятно, будут быстрее. Определяющим вопросом при выборе подхода SQL или не-SQL является то, где находятся данные и где они должны в конечном итоге находиться. Если мы просто конвертируем файл для передачи по «цепочке», лучше подходят внешние программы. Если у нас есть или нужны данные на сервере SQL, только в редких случаях целесообразно экспортировать и обрабатывать их извне.

person mjv    schedule 23.09.2009
comment
Как бы вы сформулировали этот запрос как SQL? И, как оказалось, этот скрипт на Python работает примерно в два раза быстрее, чем select * from foo into outfile 'bar' в MySQL, который вообще не обрабатывает данные. - person Kragen Javier Sitaker; 23.09.2009
comment
@Kragen, я опубликую через несколько часов. обещал! (нужно бежать...) Ориентировочно, ваши первые попытки с SQL были медленными из-за отсутствия индексов или плохо составленного запроса? - person mjv; 23.09.2009
comment
Нет, select * from foo без предложения where не работает медленно из-за отсутствия индексов; нет никакого индекса, который мог бы ускорить это. Вы можете возразить, что это плохо составленный запрос в том смысле, что он обязательно должен последовательно сканировать всю таблицу, но моя точка зрения заключается в том, что просто последовательное сканирование всей таблицы с вращающейся ржавчиной выполняется намного медленнее в MySQL. чем на самом деле выполнять весь запрос в Python из текстового файла, и я думаю, что сценарий Python все еще на несколько порядков медленнее, чем в идеале. - person Kragen Javier Sitaker; 23.09.2009
comment
@Kragen Получил фрагмент SQL [моя повседневная работа мешает моему SO-ing :-)]... Обратите внимание, что это было протестировано только в MSSQL, должно быть примерно то же самое в MySql; Я получу доступ к своему серверу позже сегодня, если это было проблемой. - person mjv; 24.09.2009
comment
Теперь я понимаю ваш SQL. Насколько хорошо MSSQL обрабатывает ключи aa, содержащие несколько тысяч строк? Запрос выглядит как O(N²) по количеству значений с определенным ключом aa, и кажется нетривиальным для оптимизации (я думаю, оптимизатору придется выяснить, что условие соединения приводит к общему упорядочению строк с одним и тем же ключом aa, что верно только в том случае, если нет повторяющихся строк, что вы также предположили и что оказалось правдой), поэтому мне интересно, используете ли вы программное обеспечение, которое действительно может справиться с этим разумно? - person Kragen Javier Sitaker; 24.09.2009
comment
@Kragen Смотрите два моих дополнения. Они основаны на SQL, но используют процедурный стиль. Я запускал их на сервере MSSQL, так как именно здесь у меня был легкий доступ к большим базам данных, где я мог эмулировать вашу нагрузку. То же самое прекрасно работало бы в MySQL, если бы не случайная синтаксическая разница. Последний скрипт выполняется чуть больше минуты для 61 млн строк. - person mjv; 25.09.2009

Вы можете использовать более умные структуры данных и по-прежнему использовать python. Я запустил вашу эталонную реализацию и свою реализацию на Python на своей машине и даже сравнил вывод, чтобы быть уверенным в результатах.

Это ваша:

$ time python ./ref.py  < data-large.txt  > ref-large.txt

real 1m57.689s
user 1m56.104s
sys 0m0.573s

Это мое:

$ time python ./my.py  < data-large.txt  > my-large.txt

real 1m35.132s
user 1m34.649s
sys 0m0.261s
$ diff my-large.txt ref-large.txt 
$ echo $?
0

А это источник:

#!/usr/bin/python
# -*- coding: utf-8; -*-
import sys
import heapq

top_5 = {}

for line in sys.stdin:
    aa, bb, cc = line.split()

    # We want the top 5 for each distinct value of aa.  There are
    # hundreds of thousands of values of aa.
    bb = float(bb)
    if aa not in top_5: top_5[aa] = []
    current = top_5[aa]
    if len(current) < 5:
        heapq.heappush(current, (bb, cc))
    else:
        if current[0] < (bb, cc):
            heapq.heapreplace(current, (bb, cc))

for aa in top_5:
    current = top_5[aa]
    while len(current) > 0:
        bb, cc = heapq.heappop(current)
        print aa, bb, cc

Обновление: знайте свои пределы. Я также рассчитал код noop, чтобы узнать самое быстрое решение для Python с кодом, похожим на оригинал:

$ time python noop.py < data-large.txt  > noop-large.txt

real    1m20.143s
user    1m19.846s
sys 0m0.267s

И сам noop.py:

#!/usr/bin/python
# -*- coding: utf-8; -*-
import sys
import heapq

top_5 = {}

for line in sys.stdin:
    aa, bb, cc = line.split()

    bb = float(bb)
    if aa not in top_5: top_5[aa] = []
    current = top_5[aa]
    if len(current) < 5:
        current.append((bb, cc))

for aa in top_5:
    current = top_5[aa]
    current.sort()
    for bb, cc in current[-5:]:
        print aa, bb, cc
person abbot    schedule 24.09.2009
comment
Хороший! Я не пробовал heapq, потому что думал, что он реализован на чистом Python и поэтому, вероятно, будет медленнее, но это не так. - person Kragen Javier Sitaker; 24.09.2009
comment
Я не тестировал его, но я считаю, что даже реализация Python heapq будет быстрее, особенно с psyco. - person abbot; 25.09.2009
comment
Он возвращает «нижние 5», а не «лучшие 5», не так ли? - person ygrek; 25.09.2009
comment
ygrek: нет, heapq поддерживает минимальную, а не максимальную кучу; current[0] — это наименьший из пяти элементов в куче, а не самый большой. Поэтому всякий раз, когда мы находим элемент ниже текущего минимума, мы заменяем им текущий минимум с помощью heapreplace. - person Kragen Javier Sitaker; 25.09.2009

На моей машине это заняло 45,7 с с 27 млн ​​строк данных, которые выглядели так:

42 0.49357 0
96 0.48075 1
27 0.640761 2
8 0.389128 3
75 0.395476 4
24 0.212069 5
80 0.121367 6
81 0.271959 7
91 0.18581 8
69 0.258922 9

Ваш скрипт занял 1m42 на этих данных, пример c++ тоже 1m46 (g++ t.cpp -o t для его компиляции, я ничего не знаю о c++).

Java 6, не то, чтобы это имело большое значение. Результат не идеален, но это легко исправить.

package top5;

import java.io.BufferedReader;
import java.io.FileReader;
import java.util.Arrays;
import java.util.Map;
import java.util.TreeMap;

public class Main {

    public static void main(String[] args) throws Exception {
        long start  = System.currentTimeMillis();
        Map<String, Pair[]> top5map = new TreeMap<String, Pair[]>();
        BufferedReader br = new BufferedReader(new FileReader("/tmp/file.dat"));

        String line = br.readLine();
        while(line != null) {
            String parts[] = line.split(" ");

            String key = parts[0];
            double score = Double.valueOf(parts[1]);
            String value = parts[2];
            Pair[] pairs = top5map.get(key);

            boolean insert = false;
            Pair p = null;
            if (pairs != null) {
                insert = (score > pairs[pairs.length - 1].score) || pairs.length < 5;
            } else {
                insert = true;
            }
            if (insert) {
                p = new Pair(score, value);
                if (pairs == null) {
                    pairs = new Pair[1];
                    pairs[0] = new Pair(score, value);
                } else {
                    if (pairs.length < 5) {
                        Pair[] newpairs = new Pair[pairs.length + 1];
                        System.arraycopy(pairs, 0, newpairs, 0, pairs.length);
                        pairs = newpairs;
                    }
                    int k = 0;
                    for(int i = pairs.length - 2; i >= 0; i--) {
                        if (pairs[i].score <= p.score) {
                            pairs[i + 1] = pairs[i];
                        } else {
                            k = i + 1;
                            break;
                        }
                    }
                    pairs[k] = p;
                }
                top5map.put(key, pairs);
            }
            line = br.readLine();
        }
        for(Map.Entry<String, Pair[]> e : top5map.entrySet()) {
            System.out.print(e.getKey());
            System.out.print(" ");
            System.out.println(Arrays.toString(e.getValue()));
        }
        System.out.println(System.currentTimeMillis() - start);
    }

    static class Pair {
        double score;
        String value;

        public Pair(double score, String value) {
            this.score = score;
            this.value = value;
        }

        public int compareTo(Object o) {
            Pair p = (Pair) o;
            return (int)Math.signum(score - p.score);
        }

        public String toString() {
            return String.valueOf(score) + ", " + value;
        }
    }
}

Скрипт AWK для подделки данных:

BEGIN {
 for (i = 0; i < 27000000; i++) {
  v = rand();
  k = int(rand() * 100);
  print k " " v " " i;
 }
 exit;
}
person Will Hartung    schedule 23.09.2009
comment
Хм, кажется, он разбивается на пробелы? Мой реальный файл данных разделен табуляцией, поэтому я изменил его, чтобы использовать "\t" в качестве разделителя. Я скомпилировал его, поместив этот файл в top5/Main.java, скомпилировав с помощью javac top5/Main.java и выполнив с помощью java top5.Main. Скоро сообщу о своих результатах. Я хотел бы поблагодарить за сценарий awk — это было важно! - person Kragen Javier Sitaker; 27.09.2009
comment
Также отмечу, что формат вывода не совместим, но это поправимо. Предполагая, что программа верна, мой первый запуск занимает 106 секунд в файле данных, на который моя программа на Python уходит 169±4 секунды. - person Kragen Javier Sitaker; 27.09.2009
comment
И это был самый медленный запуск из пяти — я получаю 96 секунд ±10, почти так же быстро, как версия OCaml. - person Kragen Javier Sitaker; 27.09.2009

Это скетч на Common Lisp

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

вторая версия

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

(defun read-a-line (stream)
  (let ((line (read-line stream nil nil)))
    (flet ((delimiter-p (c)
             (or (char= c #\space) (char= c #\tab))))
      (when line
        (let* ((s0 (position-if     #'delimiter-p line))
               (s1 (position-if-not #'delimiter-p line :start s0))
               (s2 (position-if     #'delimiter-p line :start (1+ s1)))
               (s3 (position-if     #'delimiter-p line :from-end t)))
          (values (subseq line 0 s0)
                  (list (read-from-string line nil nil :start s1 :end s2)
                        (subseq line (1+ s3)))))))))

Вышеуказанная функция возвращает два значения: ключ и список остальных.

(defun dbscan (top-5-table stream)
   "get triples from each line and put them in the hash table"
  (loop with aa = nil and bbcc = nil do
    (multiple-value-setq (aa bbcc) (read-a-line stream))
    while aa do
    (setf (gethash aa top-5-table)
          (let ((l (merge 'list (gethash aa top-5-table) (list bbcc)
                          #'> :key #'first)))
             (or (and (nth 5 l) (subseq l 0 5)) l)))))


(defun dbprint (table output)
  "print the hashtable contents"
  (maphash (lambda (aa value)
              (loop for (bb cc) in value
                    do (format output "~a ~a ~a~%" aa bb cc)))
           table))

(defun dbsum (input &optional (output *standard-output*))
  "scan and sum from a stream"
  (let ((top-5-table (make-hash-table :test #'equal)))
    (dbscan top-5-table input)
    (dbprint top-5-table output)))


(defun fsum (infile outfile)
   "scan and sum a file"
   (with-open-file (input infile :direction :input)
     (with-open-file (output outfile
                      :direction :output :if-exists :supersede)
       (dbsum input output))))

некоторые тестовые данные

(defun create-test-data (&key (file "/tmp/test.data") (n-lines 100000))
  (with-open-file (stream file :direction :output :if-exists :supersede)
    (loop repeat n-lines
          do (format stream "~a ~a ~a~%"
                     (random 1000) (random 100.0) (random 10000)))))

; (создать-тестовые-данные)

(defun test ()
  (time (fsum "/tmp/test.data" "/tmp/result.data")))

третья версия, LispWorks

Использует некоторые функции SPLIT-STRING и PARSE-FLOAT, в противном случае общий CL.

(defun fsum (infile outfile)
  (let ((top-5-table (make-hash-table :size 50000000 :test #'equal)))
    (with-open-file (input infile :direction :input)
      (loop for line = (read-line input nil nil)
            while line do
            (destructuring-bind (aa bb cc) (split-string '(#\space #\tab) line)
              (setf bb (parse-float bb))
              (let ((v (gethash aa top-5-table)))
                (unless v
                  (setf (gethash aa top-5-table)
                        (setf v (make-array 6 :fill-pointer 0))))
                (vector-push (cons bb cc) v)
                (when (> (length v) 5)
                  (setf (fill-pointer (sort v #'> :key #'car)) 5))))))
    (with-open-file (output outfile :direction :output :if-exists :supersede)
      (maphash (lambda (aa value)
                 (loop for (bb . cc) across value do
                       (format output "~a ~f ~a~%" aa bb cc)))
               top-5-table))))    
person Community    schedule 23.09.2009
comment
Для будущих читателей: ww.telent.net/cliki/SPLIT-SEQUENCE не работает связь; вместо этого используйте cliki.net/SPLIT-SEQUENCE. - person Kragen Javier Sitaker; 24.09.2009
comment
...и в Ubuntu я могу apt-get install cl-split-sequence, а затем я могу (require 'split-sequence) в SBCL. - person Kragen Javier Sitaker; 24.09.2009
comment
Еще одно замечание: чтобы это заработало, нужно заменить все вхождения ˜ на ~. - person Kragen Javier Sitaker; 24.09.2009
comment
Кроме того, порядок аргументов sort и gethash неверен. - person Kragen Javier Sitaker; 24.09.2009
comment
У меня нет with-input-from-file в SBCL; вместо этого я использую (with-open-file (stream file :direction :input). - person Kragen Javier Sitaker; 24.09.2009
comment
Версия, которую мне нужно запустить, находится по адресу gist.github.com/192354 — не похоже, так быстро, как я надеялся. В SBCL потребовалось 940 секунд, чтобы переварить 27 миллионов записей, что, похоже, было сделано правильно. Использование памяти было разумным (возможно, 83 мегабайта). SBCL сообщает, что он использовал 32 гигабайта памяти, что мне кажется много; входной файл всего 0,6 гигабайта. На той же машине интерпретируемая программа Python заняла 361 секунду. Выходные данные не совсем одинаковы, но различия выглядят нормально. Должен ли я сделать что-то по-другому? - person Kragen Javier Sitaker; 24.09.2009
comment
Спросите в sbcl-help, чтобы получить подсказки по оптимизации. - person Leslie P. Polzer; 24.09.2009
comment
Эй, как я уже указал в своей сути, разделителем может быть также #\Tab или несколько пробелов. Я получаю (min (position #\Space line) (position #\Tab line)) или есть что-то получше? - person Kragen Javier Sitaker; 24.09.2009
comment
Я обновил функцию, чтобы она принимала несколько пробелов, а также вкладки. функция становится больше, но такова жизнь... - person Rainer Joswig; 25.09.2009
comment
(defun read-a-line (stream) читать строку, анализируя первые две лексемы и получая остальные в виде строки. (let ((aa (read stream nil)) (bb (read stream nil)) (cc (read- строковый поток ноль))) (значения aa (список bb cc)))) - person Svante; 25.09.2009
comment
Сванте, ну может быть. Если вы сейчас содержание полей, да. Но иначе, может быть, и нет. a(b) в первом поле требует двух READ. - person Rainer Joswig; 25.09.2009
comment
Если вы знаете содержимое полей, то да. Но иначе, может быть, и нет. a(b) в первом поле требует двух READ. - person Rainer Joswig; 25.09.2009
comment
Потрясающе, спасибо. Есть ли что-то особенное, по вашему мнению, что я должен сделать, чтобы заставить SBCL работать более эффективно (параметры компилятора, объявления)? Сейчас тестирую новую версию... - person Kragen Javier Sitaker; 27.09.2009
comment
... хорошо, без каких-либо специальных действий в SBCL это занимает 478 секунд по сравнению со 169 секундами для версии Python (теперь, когда мой процессор не перегревается и не тормозит, хех). 26 секунд в GC (это заняло 29 гигабайт, поэтому изменения в обработке ввода не купили столько, сколько я надеялся). Я чувствую, что должен делать что-то не так, потому что SBCL должен быть немного быстрее... - person Kragen Javier Sitaker; 27.09.2009
comment
У меня есть версия для LispWorks, которая работает немного быстрее. Дорогостоящей операцией является разбор поплавка. В Common Lisp нет низкоуровневого parse-float, поэтому нужно использовать версию реализации или из библиотеки. Если я использую parse-float LispWorks, время составляет 1/3 от SBCL. - person Rainer Joswig; 27.09.2009
comment
Ух ты, значит ли это, что SBCL тратит ⅔ своего времени на разбор чисел с плавающей запятой? Кажется, это также значительная часть времени версии C++. Тем не менее, 3-кратное ускорение по-прежнему приведет его только к тому же уровню, что и интерпретируемый Python — он все равно будет значительно медленнее, чем версии Python, скомпилированные OCaml и Psyco. Я ожидал, что реализации CL, такие как Python-компилятор, смогут побить штаны Python-языка в такого рода вещах. - person Kragen Javier Sitaker; 28.09.2009
comment
Нет, SBCL теряет время в нескольких местах. Я не совсем уверен, в чем проблема, было бы целесообразно проверить это с разработчиками SBCL. Я мог бы подумать, что это также как-то связано с операциями Unicode - просто предположение. Я думаю, что ваши ожидания были ошибочными. Такие операции, как анализ поплавков (пример), являются дорогостоящими. Но это написано не на Python, а на C и вызывается Python. Там, где реализации Lisp, такие как SBCL (и многие другие), имеют тенденцию выполнять большинство операций на Lisp, а не на C. Не следует ожидать, что каждая задача, в которой Python в основном вызывает свои подпрограммы C, будет «медленной». - person Rainer Joswig; 28.09.2009
comment
Итак, вот хорошая ветка с полезными советами по SBCL: группы. google.com/group/sbcl-devel/browse_thread/thread/ - person Rainer Joswig; 29.09.2009

Вот еще одна версия OCaml, ориентированная на скорость, с пользовательским парсером для потоков. Слишком длинно, но части парсера можно использовать повторно. Спасибо peufeu за создание конкуренции :)

Скорость :

  • простой окамл - 27 сек
  • ocaml с потоковым парсером — 15 сек.
  • c с ручным парсером - 5 сек

Компилировать с:

ocamlopt -pp camlp4o code.ml -o caml

Код :

open Printf

let cmp x y = compare (fst x : float) (fst y)
let digit c = Char.code c - Char.code '0'

let rec parse f = parser
  | [< a=int; _=spaces; b=float; _=spaces; 
       c=rest (Buffer.create 100); t >] -> f a b c; parse f t
  | [< >] -> ()
and int = parser
  | [< ''0'..'9' as c; t >] -> int_ (digit c) t
  | [< ''-'; ''0'..'9' as c; t >] -> - (int_ (digit c) t)
and int_ n = parser
  | [< ''0'..'9' as c; t >] -> int_ (n * 10 + digit c) t
  | [< >] -> n
and float = parser
  | [< n=int; t=frem; e=fexp >] -> (float_of_int n +. t) *. (10. ** e)
and frem = parser
  | [< ''.'; r=frem_ 0.0 10. >] -> r
  | [< >] -> 0.0
and frem_ f base = parser
  | [< ''0'..'9' as c; t >] -> 
      frem_ (float_of_int (digit c) /. base +. f) (base *. 10.) t
  | [< >] -> f
and fexp = parser
  | [< ''e'; e=int >] -> float_of_int e
  | [< >] -> 0.0
and spaces = parser
  | [< '' '; t >] -> spaces t
  | [< ''\t'; t >] -> spaces t
  | [< >] -> ()
and crlf = parser
  | [< ''\r'; t >] -> crlf t
  | [< ''\n'; t >] -> crlf t
  | [< >] -> ()
and rest b = parser
  | [< ''\r'; _=crlf >] -> Buffer.contents b
  | [< ''\n'; _=crlf >] -> Buffer.contents b
  | [< 'c; t >] -> Buffer.add_char b c; rest b t
  | [< >] -> Buffer.contents b

let () =
  let all = Array.make 200 [] in
  let each a b c =
    assert (a >= 0 && a < 200);
    match all.(a) with
    | [] -> all.(a) <- [b,c]
    | (bmin,_) as prev::tl -> if b > bmin then
      begin
        let m = List.sort cmp ((b,c)::tl) in
        all.(a) <- if List.length tl < 4 then prev::m else m
      end
  in
  parse each (Stream.of_channel stdin);
  Array.iteri 
    (fun a -> List.iter (fun (b,c) -> printf "%i %f %s\n" a b c))
    all
person ygrek    schedule 01.10.2009

Из всех программ в этой теме, которые я тестировал до сих пор, версия OCaml является самой быстрой, а также одной из самых коротких. (Измерения на основе строк кода немного нечеткие, но они не очевидно длиннее, чем версии Python или C или C++, и они явно быстрее. )

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

Вот тайминги для различных версий на данный момент, работающих с файлом входных данных с 27 миллионами строк и 630 мегабайтами. Я использую Ubuntu Intrepid Ibex на двухъядерном процессоре Celeron 1,6 ГГц с 32-разрядной версией ОС (драйвер Ethernet был сломан в 64-разрядной версии). Я запускал каждую программу пять раз и сообщаю, сколько времени потребовалось для этих пяти попыток. Я использую Python 2.5.2, OpenJDK 1.6.0.0, OCaml 3.10.2, GCC 4.3.2, SBCL 1.0.8.debian и Octave 3.0.1.

  • Версия SquareCog для Pig: еще не тестировалась (потому что я не могу просто apt-get install pig), 7 строк кода.
  • чистая SQL-версия mjv: еще не тестировалась, но я предполагаю, что время выполнения составит несколько дней; 7 строк кода.
  • Версия OCaml от ygrek: 68,7 секунды ±0,9 в 15 строках кода.
  • Моя версия Python: 169 секунд ±4 или 86 секунд ±2 с Psyco, в 16 строках кода.
  • версия Python на основе кучи от abbot: 177 секунд ±5 в 18 строках кода или 83 секунды ±5 с Psyco.
  • Моя версия C ниже, составленная с помощью GNU sort -n: 90 + 5,5 секунд (±3, ±0,1), но дает неправильный ответ из-за недостатка GNU sort в 22 строк кода (включая одну строку оболочки.)
  • Версия hrnt на C++: 217 секунд ±3 в 25 строках кода.
  • Альтернативный процедурный подход mjv на основе SQL: еще не тестировался, 26 строк кода.
  • первый процедурный подход mjv на основе SQL: еще не тестировался, 29 строк кода.
  • Python-версия с Psyco: 181 секунда< /strong> ±4, где-то около 30 строк кода.
  • Версия Common Lisp Райнера Джосвига: 478 секунд (запускается только один раз) в 42 строках кода.
  • noop.py аббата, который преднамеренно дает неверные результаты для установления нижней границы: еще не проверено, 15 строк кода.
  • Java-версия Уилла Хартунга: 96 секунд ±10 дюймов, согласно SLOCCount Дэвида А. Уилера, 74 строки кода.
  • Версия Грега для Matlab: не работает.
  • Предложение Шайлер Эрле по использованию Pyrex в одной из версий Python: еще не пробовал.

Я подозреваю, что версия аббата у меня получается относительно хуже, чем у них, потому что реальный набор данных имеет крайне неравномерное распределение: как я уже сказал, некоторые значения aa («игроки») имеют тысячи строк, а другие — только одну.

О Psyco: я применил Psyco к своему исходному коду (и версии аббата), поместив его в функцию main, которая сама по себе сократила время примерно до 140 секунд, и вызывая psyco.full() перед вызовом main(). Это добавило около четырех строк кода.

Я могу почти решить проблему с помощью GNU sort следующим образом:

kragen@inexorable:~/devel$ time LANG=C sort -nr infile -o sorted

real    1m27.476s
user    0m59.472s
sys 0m8.549s
kragen@inexorable:~/devel$ time ./top5_sorted_c < sorted > outfile

real    0m5.515s
user    0m4.868s
sys 0m0.452s

Здесь top5_sorted_c это короткая программа на C:

#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

enum { linesize = 1024 };

char buf[linesize];
char key[linesize];             /* last key seen */

int main() {
  int n = 0;
  char *p;

  while (fgets(buf, linesize, stdin)) {
    for (p = buf; *p && !isspace(*p); p++) /* find end of key on this line */
      ;
    if (p - buf != strlen(key) || 0 != memcmp(buf, key, p - buf)) 
      n = 0;                    /* this is a new key */
    n++;

    if (n <= 5)               /* copy up to five lines for each key */
      if (fputs(buf, stdout) == EOF) abort();

    if (n == 1) {               /* save new key in `key` */
      memcpy(key, buf, p - buf);
      key[p-buf] = '\0';
    }
  }
  return 0;
}

Сначала я попытался написать эту программу на C++ следующим образом и получил время выполнения, которое было значительно медленнее, 33,6 ± 2,3 секунды вместо 5,5 ± 0,1 секунды:

#include <map>
#include <iostream>
#include <string>

int main() {
  using namespace std;
  int n = 0;
  string prev, aa, bb, cc;

  while (cin >> aa >> bb >> cc) {
    if (aa != prev) n = 0;
    ++n;
    if (n <= 5) cout << aa << " " << bb << " " << cc << endl;
    prev = aa;
  }
  return 0;
}

Я сказал почти. Проблема в том, что sort -n подходит для большинства данных, но терпит неудачу, когда пытается сравнить 0.33 с 3.78168e-05. Поэтому, чтобы получить такую ​​​​производительность и действительно решить проблему, мне нужна лучшая сортировка.

В любом случае, мне кажется, что я ною, но подход с сортировкой и фильтрацией примерно в 5 раз быстрее, чем программа на Python, в то время как элегантная программа STL от hrnt на самом деле немного медленнее — кажется, есть какая-то грубая неэффективность в <iostream>. Я не знаю, куда идут остальные 83% времени выполнения в этой маленькой C++ версии фильтра, но он не приносит никакой пользы, что заставляет меня подозревать, что я не знаю, куда он идет и в версии std::map hrnt. . Можно ли и эту версию ускорить в 5 раз? Потому что это было бы очень круто. Его рабочий набор может быть больше моего кеша L2, но, как оказалось, это не так.

Некоторое исследование с callgrind показало, что моя программа-фильтр на C++ выполняет 97% своих инструкций внутри operator >>. Я могу идентифицировать как минимум 10 вызовов функций на входной байт, и cin.sync_with_stdio(false); не помогает. Это, вероятно, означает, что я мог заставить программу hrnt C работать значительно быстрее, более эффективно анализируя входные строки.

Редактировать: kcachegrind утверждает, что программа hrnt выполняет 62% своих инструкций (в небольшом входном файле из 157000 строк), извлекая doubles из istream. Существенная часть этого связана с тем, что библиотека istreams, по-видимому, выполняет около 13 вызовов функций на входной байт при попытке проанализировать файл double. Безумный. Могу ли я неправильно понять вывод kcachegrind?

В любом случае, есть другие предложения?

person Community    schedule 24.09.2009
comment
Действительно, реализации iostreams на C++ исторически были довольно медленными. Однако они больше не должны быть такими медленными :) Кстати, какие версии g++ и libstdc++ вы используете? - person hrnt; 27.09.2009
comment
Спасибо! Я использую libstdc++6 4.3.2-1ubuntu12 и g++ 4:4.3.1-1ubuntu2. - person Kragen Javier Sitaker; 28.09.2009
comment
на самом деле вы можете получить Pig, просто нужно получить его с archive.cloudera.com, но не Не беспокойтесь, я запустил его, и это занимает 20 минут. Pig создан для задач, которые не помещаются в памяти, в данном случае он крайне неэффективен. - person SquareCog; 02.11.2009
comment
SquareCog: Это позор! Интересно, что он делал эти 20 минут? - person Kragen Javier Sitaker; 05.11.2009

Довольно простой Caml (27 * 10^6 строк — 27 секунд, C++ от hrnt — 29 секунд)

open Printf
open ExtLib

let (>>) x f = f x
let cmp x y = compare (fst x : float) (fst y)
let wsp = Str.regexp "[ \t]+"

let () =
  let all = Hashtbl.create 1024 in
  Std.input_lines stdin >> Enum.iter (fun line ->
    let [a;b;c] = Str.split wsp line in
    let b = float_of_string b in
    try
      match Hashtbl.find all a with
      | [] -> assert false
      | (bmin,_) as prev::tl -> if b > bmin then
        begin
          let m = List.sort ~cmp ((b,c)::tl) in
          Hashtbl.replace all a (if List.length tl < 4 then prev::m else m)
        end
    with Not_found -> Hashtbl.add all a [b,c]
  );
  all >> Hashtbl.iter (fun a -> List.iter (fun (b,c) -> printf "%s %f %s\n" a b c))
person ygrek    schedule 25.09.2009
comment
Это довольно коротко! Как он обрабатывает случай, когда поля ввода разделены табуляцией или несколькими пробелами? И если он медленнее, чем версия C++, значит ли это, что он медленнее, чем версия Python? - person Kragen Javier Sitaker; 25.09.2009
comment
Оригинальная версия Python здесь занимает 1 мин 20 с. Отредактировал код для обработки нескольких пробелов, спасибо за подсказку. Кстати, я действительно думаю, что эта задача ограничена вводом-выводом, лучшая производительность может быть достигнута за счет распараллеливания. - person ygrek; 25.09.2009
comment
Это не близко к привязке к вводу-выводу. На моей машине моя версия Python занимает 5–7 минут, в то время как мой фильтр C обрабатывает те же данные за 5–7 секунд. Но это без учета времени на сортировку. (Данные составляют 630 МБ в текстовой форме, и у меня 1 ГБ ОЗУ, поэтому сортировка также не делает их связанными с вводом-выводом. Возможно, с пропускной способностью основной памяти.) - person Kragen Javier Sitaker; 26.09.2009
comment
Кстати, спасибо за ссылку на ExtLib! Там есть много вещей, которые выглядят интересно. Я получил вашу программу для байтовой компиляции с помощью ocamlc -I +extlib str.cma extLib.cma top5.ml (я немного заржавел с OCaml, поэтому мне потребовалось некоторое время, чтобы понять это, особенно заглавная L). Я скомпилировал его в машинный код с помощью ocamlopt -I +extlib str.cmxa extLib.cmxa top5.ml, и он работает за 69 секунд (моя версия Python — 169±4, версия C++ — 217±3). Это делает его самой быстрой версией, которую я когда-либо пробовал, и одной из самых коротких здесь. Какая у вас ОС? Вы просто байт-компилировали это? - person Kragen Javier Sitaker; 27.09.2009
comment
Я действительно удивлен вашими результатами - вот мои тайминги (на тестовых данных, сгенерированных сценарием awk): c++ 0m30.559s py(heapq) 1m10.400s ocaml 0m40.470s Возможно, некоторую разницу можно объяснить разницей в данных, но ваш результат C++ выглядит действительно странно. ОС — Debian x86_64 OCaml 3.11.1 (скомпилирован в собственный код) g++ 4.3.2 (-O2) python 2.5.2 - person ygrek; 28.09.2009
comment
Забавно : из этих 40 секунд : 4 секунд тратится на чтение строк, 16 с на разбиение, 4 с на преобразование в число с плавающей запятой, 16 с — вычисление top5. Похоже, есть много возможностей для оптимизации :) - person ygrek; 28.09.2009
comment
Я использую версию того же g++ для Ubuntu, но в 32-битном режиме. - person Kragen Javier Sitaker; 28.09.2009
comment
Гипотеза: возможно, версия C++ на самом деле тратит значительную часть своего времени на вставку в карты в моих тестах из-за дистрибутива Zipf, но не в вашем? И, может быть, отсортированный связанный список дешевле, чем красно-черное дерево (или что-то еще, что использует версия C++), когда N = 5? - person Kragen Javier Sitaker; 01.10.2009
comment
Отредактировал код для сортировки списка только тогда, когда это действительно необходимо (41 -> 27 сек). Другие оптимизации, а именно -- переход с hashtbl на массив, разделение с помощью pcre, массивы вместо списков для topN -- не дают существенного ускорения. - person ygrek; 01.10.2009

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

[править] Благодаря тестовым данным, предоставленным awk-скриптом в этой ветке, мне удалось немного почистить и ускорить код. Я не пытаюсь найти самую быструю возможную версию - цель состоит в том, чтобы предоставить достаточно быструю версию, которая не такая уродливая, как люди думают, могут быть решения STL.

Эта версия должна быть примерно в два раза быстрее первой версии (проходит 27 миллионов строк примерно за 35 секунд). Пользователи Gcc, не забудьте скомпилировать это с параметром -O2.

#include <map>
#include <iostream>
#include <functional>
#include <utility>
#include <string>
int main() {
  using namespace std;
  typedef std::map<string, std::multimap<double, string> > Map;
  Map m;
  string aa, cc;
  double bb;
  std::cin.sync_with_stdio(false); // Dunno if this has any effect, but anyways.

  while (std::cin >> aa >> bb >> cc)
    {
      if (m[aa].size() == 5)
        {
          Map::mapped_type::iterator iter = m[aa].begin();
          if (bb < iter->first)
            continue;
          m[aa].erase(iter);
        }
      m[aa].insert(make_pair(bb, cc));
    }
  for (Map::const_iterator iter = m.begin(); iter != m.end(); ++iter)
    for (Map::mapped_type::const_iterator iter2 = iter->second.begin();
         iter2 != iter->second.end();
         ++iter2)
      std::cout << iter->first << " " << iter2->first << " " << iter2->second <<
 std::endl;

}
person hrnt    schedule 23.09.2009
comment
Если вы компилируете это с помощью g++, вам следует использовать -O2, чтобы включить оптимизацию компилятора. - person hrnt; 24.09.2009
comment
Хм, обязательно будет стираться пара с наименьшим bb при m[aa].erase(--iter)? - person Kragen Javier Sitaker; 24.09.2009
comment
(Примечание: я обновил версию, которая, возможно, стала немного чище) В старой версии была карта с ключом bb. C++ сортирует карты по их ключу - в старой версии порядок был по убыванию. В этом случае последним элементом является элемент с наименьшим ключом (=bb). - person hrnt; 24.09.2009
comment
В этой новой версии карта отсортирована по возрастанию. Поскольку в качестве ключа используется bb, первым элементом всегда является элемент с наименьшим bb. Если новый bb больше этого, то старый можно удалить. - person hrnt; 24.09.2009
comment
Очень элегантный! Думаю, я просто не заметил, что Map::mapped_type сам был multimap. - person Kragen Javier Sitaker; 24.09.2009
comment
К сожалению, я думал, что это было быстрее, чем версия Python на моей машине, но, похоже, меня обманул перегрев процессора. Эта версия C++ компилируется с помощью g++ -O2 за 217 секунд (±3), тогда как версия Python занимает 169 секунд (±4). Я до сих пор впечатлен тем, что версия на C++ такая же чистая, как она есть, даже несмотря на то, что она все еще кажется немного более запутанной, чем Python (и в два раза длиннее). - person Kragen Javier Sitaker; 27.09.2009

Интересно, что оригинальное решение на Python, безусловно, выглядит наиболее выглядящим (хотя пример с C++ близок к этому).

Как насчет использования Pyrex или Psyco в исходном коде?

person Community    schedule 24.09.2009
comment
Psyco фактически делает версию Python быстрее, чем версии Java и традиционные версии Unix. Спасибо! - person Kragen Javier Sitaker; 27.09.2009

Кто-нибудь пробовал решить эту проблему только с помощью awk. Конкретно "мокнуть"? Согласно этому сообщению в блоге, он должен быть быстрее, чем даже Java и C++: http://anyall.org/blog/2009/09/dont-mawk-awk-the-fastest-and-most-elegant-big-data-munging-language/

РЕДАКТИРОВАТЬ: Просто хотел уточнить, что единственное утверждение, сделанное в этом сообщении в блоге, заключается в том, что для определенного класса проблем, которые специально подходят для обработки в стиле awk, виртуальная машина mawk может превзойти «ванильные» реализации на Java и С++.

person Navin    schedule 24.09.2009
comment
Это действительно отличный пост в блоге! Спасибо! Я еще не пробовал использовать mawk для этого. Я не думаю, что мы можем оправдать всеобщее ожидание, что mawk всегда быстрее, чем Java и C++ — в конце концов, все, что вы можете сделать в интерпретаторе mawk, можно сделать и в C++ — но определенно стоит попробовать. Еще одна приятная особенность mawk — это его небольшой размер: всего около 10 тыс. единиц C. - person Kragen Javier Sitaker; 24.09.2009
comment
Согласен, что общее заявление не оправдано, и я не думаю, что в этом сообщении в блоге делается такое заявление. Утверждается, что awk — это предметно-ориентированный язык (для задач построчной обработки данных), и для таких задач виртуальная машина mawk может превзойти ванильные программы на Java и C++. Я отредактировал свой первоначальный комментарий, включив в него это уточнение. - person Navin; 24.09.2009
comment
Интересно, как бы вы справились с сортировкой в ​​mawk. Кажется, у него нет встроенной процедуры сортировки. У него есть массивы, поэтому вы можете написать один, но процедура интерпретируемой сортировки (или процедура управления кучей), по-видимому, ставит awk в невыгодное положение как по скорости, так и по краткости. - person Kragen Javier Sitaker; 25.09.2009

Поскольку вы спросили о Matlab, вот как я сделал что-то вроде того, о чем вы просите. Я пытался сделать это без циклов for, но он у меня есть, потому что мне не хотелось долго с ним работать. Если вас беспокоит память, вы можете извлекать данные из потока порциями с помощью fscanf, а не читать весь буфер.

fid = fopen('fakedata.txt','r');
tic
A=fscanf(fid,'%d %d %d\n');
A=reshape(A,3,length(A)/3)';  %Matlab reads the data into one long column'
Names = unique(A(:,1));
for i=1:length(Names)
    indices = find(A(:,1)==Names(i));   %Grab all instances of key i
    [Y,I] = sort(A(indices,2),1,'descend'); %sort in descending order of 2nd record
    A(indices(I(1:min([5,length(indices(I))]))),:) %Print the top five
end
toc
fclose(fid)
person Community    schedule 24.09.2009
comment
Да, я думал о Matlab в смысле отсутствия вонючих петель ☺ — Может быть, это свинья, я не знаю. - person Kragen Javier Sitaker; 25.09.2009
comment
Он делает миллион записей за 3,91 секунды. Я также исправил ошибку в коде. - person ; 25.09.2009
comment
Насколько быстра версия Python на машине, где Matlab делает миллион записей за 3,91 секунды? - person Kragen Javier Sitaker; 25.09.2009
comment
Я заставил его работать с моим примером набора данных из исходного поста, изменив строку формата на '%s %f %s\n', но это преобразует первый и третий столбцы в строки переменной длины байтовых чисел ASCII! Это работает с моим образцом данных, потому что все эти столбцы имеют длину в один символ, но не для реальных данных. Кроме того, формат вывода немного отличается — он печатает ASCII-номер символа и перемежает ans = строк. - person Kragen Javier Sitaker; 27.09.2009
comment
Таким образом, альтернативной формулировкой для обработки строк переменной длины может быть fid = fopen('fakedata.txt','r'); tic A=textscan(fid,'%s %f %s\n'); Имена = уникальные (А {1}); для i = 1: длина (имена) index = strmatch (имена {i}, A {1}); %Захватить все экземпляры ключа i [Y,I] = sort(A{2}(indices),1,'descend'); %сортировать в порядке убывания индексов 2-й записи = индексы (I (1: мин ([5, длина (индексы (I))]))); % disp(sprintf('%s %f %s',A{1}{indices},A{2}(indices),A{3}{indices})); конец списка fclose(fid) - person ; 29.09.2009
comment
Ну, это не сработало. Во всяком случае, вместо использования fscanf я использовал textscan. Textscan втиснет данные в массив ячеек, поэтому он может справиться со строкой переменной длины. Однако распечатка данных теперь становится проблемой, и я не нашел для этого элегантного решения. Но все это немного спорно, потому что игнорирование печати, как это делает мой пример кода моего предыдущего комментария, занимает более 27 секунд. Значительную часть этого времени занимает сам textscan. Итак, для вашей конкретной задачи Matlab не является хорошим решением, но вы поняли это в первую очередь. - person ; 29.09.2009

Говоря о нижних границах времени вычислений:

Давайте проанализируем мой алгоритм выше:

for each row (key,score,id) :
    create or fetch a list of top scores for the row's key
    if len( this list ) < N
        append current
    else if current score > minimum score in list
        replace minimum of list with current row
        update minimum of all lists if needed

Пусть N будет N в верхнем N. Пусть R будет количеством строк в вашем наборе данных. Пусть K будет количеством различных ключей.

Какие предположения мы можем сделать?

R * sizeof(row) > RAM или, по крайней мере, достаточно большой, чтобы мы не хотели загружать все это, использовать хэш для группировки по ключу и сортировать каждый бин. По той же причине мы не сортируем весь материал.

Kragen любит хэш-таблицы, поэтому K * sizeof(состояние каждого ключа) ‹‹ RAM, скорее всего, он помещается в кэш L2/3.

Краген не сортирует, поэтому K*N ‹‹ R т.е. каждый ключ имеет гораздо больше, чем N записей

(примечание: A ‹‹ B означает, что A меньше B)

Если данные имеют случайное распределение, то

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

Таким образом, стоимость каждой строки составляет 1 поиск по хэшу + 1 сравнение + эпсилон * (вставка списка + (N+1) сравнений для минимума)

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

Экспериментальное доказательство:

Приведенный выше набор данных из 27 миллионов строк производит 5933 вставки в первые N списков. Все остальные строки отклоняются простым поиском и сравнением ключей. эпсилон = 0,0001

Грубо говоря, стоимость составляет 1 поиск + сравнение на строку, что занимает несколько наносекунд.

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

person bobflux    schedule 30.09.2009
comment
да. Ну, вы упустили время анализа, и реальный набор данных заканчивается несколькими сотнями тысяч окончательных элементов в первых N списках (распределение Ципфа, опять же), поэтому в них должно было быть по крайней мере несколько сотен тысяч вставок. Но этот ответ действительно хорошо объясняет мою неудовлетворенность тем, насколько далеки рабочие ответы от такого рода производительности — и направление, о, может быть, вам следует распараллелить это. - person Kragen Javier Sitaker; 30.09.2009

Разве это не так просто, как

 SELECT DISTINCT aa, bb, cc FROM tablename ORDER BY bb DESC LIMIT 5

?

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

И, конечно же, если вам все равно нужен плоский файл, вы можете использовать его.

person Toby    schedule 23.09.2009
comment
Это другой запрос. Например, если у меня есть 500000 различных значений aa, это даст мне 5 строк вывода, в то время как я хочу от 500000 до 2500000: до 5 строк вывода для каждого значения aa. - person Kragen Javier Sitaker; 23.09.2009

Выбор «топ-5» будет выглядеть примерно так. Обратите внимание, что сортировки нет. Ни один список в словаре top_5 никогда не превышает 5 элементов.

from collections import defaultdict
import sys

def keep_5( aList, aPair ):
    minbb= min( bb for bb,cc in aList )
    bb, cc = aPair
    if bb < minbb: return aList
    aList.append( aPair )
    min_i= 0
    for i in xrange(1,6):
        if aList[i][0] < aList[min_i][0]
            min_i= i
    aList.pop(min_i)
    return aList


top_5= defaultdict(list)
for row in sys.stdin:
    aa, bb, cc = row.split()
    bb = float(bb)
    if len(top_5[aa]) < 5:
        top_5[aa].append( (bb,cc) )
    else:
        top_5[aa]= keep_5( top_5[aa], (bb,cc) )
person S.Lott    schedule 23.09.2009
comment
Я рассматривал этот подход, но решил, что он будет слишком медленным, но признаю, что на самом деле я его не пробовал. Я действительно уверен, что это не даст мне ускорение порядка величины, которое я ищу. - person Kragen Javier Sitaker; 23.09.2009
comment
Запустите его и посмотрите. Это очень быстро. - person S.Lott; 23.09.2009
comment
Мой скрипт работает с определенным набором данных за 1 мин 38 с. Ваш занимает 2 м2, примерно на 20% больше времени, после того, как я исправил его на import sys и изменил line.split() на row.split(). Кроме того, он не производит никакого вывода. Вскоре я смогу сообщить, правильно ли это (в отличие от моей первоначальной версии!) - person Kragen Javier Sitaker; 24.09.2009
comment
...это тоже неправильно; он счастлив заменить любую строку, которая меньше той, которую он рассматривает, на ту, которую он рассматривает. Следовательно, если у вас есть строки с оценками (bb) 5, 4, 3, 2, 1, и вы вставите новую строку с оценкой 3, она заменит 2 на 3 вместо 1, оставив вас с 5, 4, 3, 3, 1 вместо 5, 4, 3, 3, 2. Короче говоря, он медленнее дает неправильный ответ. - person Kragen Javier Sitaker; 24.09.2009
comment
Между прочим, wc требуется 4,7 секунды, чтобы сообщить, что этот набор данных содержит 27 миллионов строк в 630 мегабайтах. - person Kragen Javier Sitaker; 24.09.2009
comment
@Kragen Хавьер Ситакер: Кроме того, он не производит никакого вывода. Правильный. Он создает ту же структуру top_5; поэтому не стесняйтесь копировать цикл print из исходного сценария. - person S.Lott; 24.09.2009
comment
Да, это то, что я сделал, чтобы узнать, что он дает неправильный ответ. - person Kragen Javier Sitaker; 24.09.2009

Версия Pig будет выглядеть примерно так (не проверено):

 Data = LOAD '/my/data' using PigStorage() as (aa:int, bb:float, cc:chararray);
 grp = GROUP Data by aa;
 topK = FOREACH grp (
     sorted = ORDER Data by bb DESC;
     lim = LIMIT sorted 5;
     GENERATE group as aa, lim;
)
STORE topK INTO '/my/output' using PigStorage();

Свинья не оптимизирована для производительности; его цель — обеспечить возможность обработки многотерабайтных наборов данных с использованием фреймворков параллельного выполнения. У него есть локальный режим, так что вы можете попробовать его, но я сомневаюсь, что он превзойдет ваш скрипт.

person SquareCog    schedule 24.09.2009
comment
Вау, это удивительно коротко и ясно, намного лучше, чем любая другая версия. Я собирался спросить, каковы накладные расходы на группу (оборотной стороной некоторых групп, состоящих из десятков тысяч членов, является то, что в большинстве групп есть один участник), но я не уверен, как это сформулировать. Я должен попробовать и посмотреть. - person Kragen Javier Sitaker; 24.09.2009

Это был хороший вызов на обеденный перерыв, хе-хе.

Top-N — известный убийца баз данных. Как показано в сообщении выше, нет способа эффективно выразить это в обычном SQL.

Что касается различных реализаций, вы должны иметь в виду, что медленная часть здесь — это не сортировка или top-N, а синтаксический анализ текста. Вы давно смотрели исходный код glibc strtod()?

Например, я получаю, используя Python:

Read data : 80.5  s
My TopN   : 34.41 s
HeapTopN  : 30.34 s

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

Если N в вашем topN невелико, например 5, реализация моего алгоритма на языке C будет, вероятно, самой быстрой. Если N может быть больше, кучи — гораздо лучший вариант.

Итак, поскольку ваши данные, вероятно, находятся в базе данных, и ваша проблема заключается в получении данных, а не в фактической обработке, если вам действительно нужен сверхбыстрый движок TopN, вам следует написать модуль C для вашего база данных на выбор. Так как postgres быстрее почти во всем, я предлагаю использовать postgres, к тому же для него несложно написать C-модуль.

Вот мой код Python:

import random, sys, time, heapq

ROWS = 27000000

def make_data( fname ):
    f = open( fname, "w" )
    r = random.Random()
    for i in xrange( 0, ROWS, 10000 ):
        for j in xrange( i,i+10000 ):
            f.write( "%d    %f    %d\n" % (r.randint(0,100), r.uniform(0,1000), j))
        print ("write: %d\r" % i),
        sys.stdout.flush()
    print

def read_data( fname ):
    for n, line in enumerate( open( fname ) ):
        r = line.strip().split()
        yield int(r[0]),float(r[1]),r[2]
        if not (n % 10000 ):
            print ("read: %d\r" % n),
            sys.stdout.flush()
    print

def topn( ntop, data ):
    ntop -= 1
    assert ntop > 0
    min_by_key = {}
    top_by_key = {}
    for key,value,label in data:
        tup = (value,label)
        if key not in top_by_key:
            # initialize
            top_by_key[key] = [ tup ]
        else:
            top = top_by_key[ key ]
            l    = len( top )
            if l > ntop:
                # replace minimum value in top if it is lower than current value
                idx = min_by_key[ key ]
                if top[idx] < tup:
                    top[idx] = tup
                    min_by_key[ key ] = top.index( min( top ) )
            elif l < ntop:
                # fill until we have ntop entries
                top.append( tup )
            else:
                # we have ntop entries in list, we'll have ntop+1
                top.append( tup )
                # initialize minimum to keep
                min_by_key[ key ] = top.index( min( top ) )

    # finalize:
    return dict( (key, sorted( values, reverse=True )) for key,values in top_by_key.iteritems() )

def grouptopn( ntop, data ):
    top_by_key = {}
    for key,value,label in data:
        if key in top_by_key:
            top_by_key[ key ].append( (value,label) )
        else:
            top_by_key[ key ] = [ (value,label) ]

    return dict( (key, sorted( values, reverse=True )[:ntop]) for key,values in top_by_key.iteritems() )

def heaptopn( ntop, data ):
    top_by_key = {}
    for key,value,label in data:
        tup = (value,label)
        if key not in top_by_key:
            top_by_key[ key ] = [ tup ]
        else:
            top = top_by_key[ key ]
            if len(top) < ntop:
                heapq.heappush(top, tup)
            else:
                if top[0] < tup:
                    heapq.heapreplace(top, tup)

    return dict( (key, sorted( values, reverse=True )) for key,values in top_by_key.iteritems() )

def dummy( data ):
    for row in data:
        pass

make_data( "data.txt" )

t = time.clock()
dummy( read_data( "data.txt" ) )
t_read = time.clock() - t

t = time.clock()
top_result = topn( 5, read_data( "data.txt" ) )
t_topn = time.clock() - t

t = time.clock()
htop_result = heaptopn( 5, read_data( "data.txt" ) )
t_htopn = time.clock() - t

# correctness checking :
for key in top_result:
    print key, " : ", "        ".join (("%f:%s"%(value,label)) for (value,label) in    top_result[key])
    print key, " : ", "        ".join (("%f:%s"%(value,label)) for (value,label) in htop_result[key])

print
print "Read data :", t_read
print "TopN :     ", t_topn - t_read
print "HeapTopN : ", t_htopn - t_read

for key in top_result:
    assert top_result[key] == htop_result[key]
person bobflux    schedule 26.09.2009
comment
Вы, безусловно, правы насчет медленной части в нескольких реализациях, представляющей собой синтаксический анализ текста. В последнее время я не просматривал исходный код strtod, но я просмотрел некоторые результаты профилирования... И правда, что самая медленная часть уже считывает данные из базы данных, но это соблазняя меня хранить его где-то в другом месте. - person Kragen Javier Sitaker; 26.09.2009
comment
... вау, небольшая программа синхронизации glibc strtod на моей машине работает со скоростью всего 20 мегабайт в секунду (для десятисимвольных строк). Учитывая, что мой 630-мегабайтный файл данных содержит 240 мегабайт текста с плавающей запятой, это составляет 12 секунд времени выполнения. Это очень много в абсолютном смысле, если не в относительном. Он должен быть в 10 раз лучше, чем это. - person Kragen Javier Sitaker; 27.09.2009
comment
Это переменная l, а не цифра 1, верно? Просто проверка. - person Kragen Javier Sitaker; 27.09.2009
comment
Интересно, будет ли psyco.full() иметь разное ускорение при разных подходах... - person Kragen Javier Sitaker; 27.09.2009
comment
В вашем тестовом наборе данных psyco.full() ускоряет оба алгоритма в 2–3 раза, и действительно, ваш алгоритм работает быстрее, чем heapq. Я еще не разбил код, чтобы измерить его на моем реальном наборе данных. Было бы неудивительно, если бы это было быстрее, чем другие подходы Python, которые я пробовал. - person Kragen Javier Sitaker; 27.09.2009
comment
...упс, это было из твоего тайминга: t_topn - t_read. По какой-то причине Psyco фактически сделал read_data медленнее, так что на самом деле это не было таким большим улучшением... - person Kragen Javier Sitaker; 27.09.2009
comment
Я написал эту функцию, чтобы иметь возможность протестировать topn с помощью Psyco на исходном наборе данных: read_silently = lambda infile: ((int(aa), float(bb), cc) for aa, bb, cc in (line.split() for line in infile)). psyco.full() ускорил программу примерно с 218 секунд до примерно 186, что является удивительно небольшой разницей, поскольку моя исходная программа ускорилась со 169 секунд до 86 секунд. - person Kragen Javier Sitaker; 27.09.2009

Я люблю вызовы на обеденный перерыв. Вот реализация за 1 час.

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

У меня завалялся код fast-atoi из предыдущего проекта, поэтому я просто импортировал его.

http://www.copypastecode.com/11541/

Этот исходный код C занимает около 6,6 секунд, чтобы разобрать 580 МБ входного текста (27 миллионов строк), половина этого времени — это fgets, лол. Затем вычисление top-n занимает примерно 0,05 секунды, но я не знаю точно, так как время, необходимое для top-n, меньше, чем шум таймера.

Вы будете тем, кто проверит его на правильность, хотя XDDDDDDDDDDDD

Интересно да?

person bobflux    schedule 28.09.2009
comment
Отлично, такая скорость, на которую я надеялся! Теперь это просто вопрос получения такой скорости из достаточно короткой программы... - person Kragen Javier Sitaker; 28.09.2009
comment
› Просто поместите все в библиотеку › и ваш код будет достаточно коротким (Java Book of Wisdom) - person bobflux; 28.09.2009
comment
Нет, просто mmap() файл, избавьтесь от fgets и немного взломайте его, я уверен, вы сможете получить его менее чем за 3 секунды. Если число с плавающей запятой ниже, чем самый низкий балл в вашей текущей таблице результатов для всех категорий, вам не нужно анализировать два других поля. И если он ниже минимума текущей категории, вам не нужно разбирать другой int. Что касается printf(), некоторое время назад я написал несколько функций преобразования целых чисел в текст, которые были примерно в 20 раз быстрее, чем printf(%d), это несложно. - person bobflux; 28.09.2009
comment
В данном конкретном случае я подозреваю, что библиотеки недостаточно, поскольку многие применяемые оптимизации в значительной степени зависят от характера данных. WRT mmap Я часто разочаровывался в том, что он не мог ускорить процесс, но, возможно, в данном случае я бы не стал. - person Kragen Javier Sitaker; 28.09.2009
comment
-1. Он не отвечает на вопрос OP, он жестко кодирует так много вещей, что это даже не смешно, он не решает проблему, а только какую-то конкретную подзадачу. Да, это быстро, но цена слишком высока. - person ygrek; 29.09.2009
comment
Проблема OP состоит в том, чтобы быстро проанализировать огромное количество чисел с двойной точностью, а затем применить к ним тривиальный алгоритм. Поскольку быстрый анализ двойников невозможен, если вы хотите получить ответ в формате IEEE, самое простое решение — использовать такой трюк. Единственная жестко запрограммированная вещь - это формат (очевидно, поскольку он был указан в OP) и количество возможных ключей (которые действительно должны обрабатываться хеш-таблицей, а не массивом C). - person bobflux; 29.09.2009
comment
Позвольте мне прочитать это для вас: чтобы он был таким же коротким (как Python ниже), но намного быстрее.. Другие жестко закодированные вещи: нет отрицательных чисел с плавающей запятой, только один вид разделителя, первый и третий столбцы только целые, ограниченная длина строки . Несомненно, отказ от абстракций и использование ограниченных массивов, проиндексированных целыми числами, вместо динамических хэш-таблиц может привести к более быстрому коду, но это выглядит как неправильное направление (лично для меня). - person ygrek; 29.09.2009
comment
сделать его таким же коротким (как Python ниже), но намного быстрее, просто невозможно, если вы не последуете первому совету, который я дал, который заключается в том, чтобы отказаться от текстового формата и либо использовать двоичный формат, либо написать модуль C для вашей базы данных по выбору, что действительно является самым быстрым способом, если ваши данные уже находятся в базе данных. Проблема OP заключается в синтаксическом анализе текстового формата, который является либо общим, либо быстрым, а не тем и другим одновременно. Алгоритм top-N сам по себе довольно тривиален, и использование хеш-таблицы (что, конечно, является правильным выбором), вероятно, очень мало повлияет на скорость. - person bobflux; 29.09.2009
comment
Поэтому я считаю этот ответ важным не потому, что он хорош как само решение, а потому, что он показывает, что нижняя граница времени выполнения решения далека, гораздо меньше, чем многие подозревали. Добавление отрицательного числа и обработки табуляции и хеш-таблицы обратно не будет иметь большого значения. Я не согласен с «либо общим, либо быстрым, но не обоими» в этом контексте — можно потерять лишь немного скорости по сравнению с этой версией, обрабатывая более общий ввод. Что касается «отбрасывания абстракций», то да, этот код должен быть сгенерирован компилятором, а не программистом. - person Kragen Javier Sitaker; 29.09.2009
comment
На самом деле, мой дерьмовый код выше был больше предназначен для демонстрации того, насколько быстрым он может стать, если вы готовы отказаться от всего здравого смысла, как и говорит Краген. Обратите внимание, что он по-прежнему проверяет все входные данные. Лучшее решение этой проблемы — изменить задачу, получив данные в другом формате. - person bobflux; 30.09.2009
comment
Краген, ваше последнее замечание абсолютно точно: общий анализ текста (например, настраиваемый парсер CSV) на самом деле является написанием вашего собственного крошечного интерпретируемого языка. Вы думаете, что это будет работать быстро, потому что вы пишете ее на C, но на самом деле ваша программа не на C, а на специальном языке (точно так же, как строки формата printf являются специальным, медленным, интерпретируемым языком). Кстати, откуда ваши данные? Мне любопытно. - person bobflux; 30.09.2009
comment
На данный момент он извлекается из MySQL, но перед тем, как войти в MySQL, он был выдан (в виде текста, в менее разборчивом формате) какой-то проприетарной программой, написанной на смеси разных скомпилированных языков. Очевидная неспособность MySQL даже последовательно просматривать записи менее чем за несколько минут (при вращающейся ржавчине) заставляет меня думать, что мне следует использовать другой носитель данных, что-то более подходящее для последовательного доступа. - person Kragen Javier Sitaker; 01.10.2009
comment
пуфе, согласен. (Кстати, как вы сказали, массив hashtbl-› не дал большого ускорения, я был удивлен) - person ygrek; 01.10.2009

Что ж, пожалуйста, возьмите кофе и прочитайте исходный код strtod - это ошеломляет, но необходимо, если вы хотите использовать float -> text -> float, чтобы вернуть тот же float, с которого вы начали.... правда...

Разбор целых чисел происходит намного быстрее (хотя не столько в python, сколько в C, да).

В любом случае, помещая данные в таблицу Postgres:

SELECT count( key ) FROM the dataset in the above program

=> 7 с (поэтому для чтения 27 млн ​​записей требуется 7 с)

CREATE INDEX topn_key_value ON topn( key, value );

191 s

CREATE TEMPORARY TABLE topkeys AS SELECT key FROM topn GROUP BY key;

12 s

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

CREATE TEMPORARY TABLE top AS SELECT (r).* FROM (SELECT (SELECT b AS r FROM topn b WHERE b.key=a.key ORDER BY value DESC LIMIT 1) AS r FROM topkeys a) foo;

Темп: 15 310 мс

INSERT INTO top SELECT (r).* FROM (SELECT (SELECT b AS r FROM topn b WHERE b.key=a.key ORDER BY value DESC LIMIT 1 OFFSET 1) AS r FROM topkeys a) foo;

Темп: 17 853 мс

INSERT INTO top SELECT (r).* FROM (SELECT (SELECT b AS r FROM topn b WHERE b.key=a.key ORDER BY value DESC LIMIT 1 OFFSET 2) AS r FROM topkeys a) foo;

Темп: 13 983 мс

INSERT INTO top SELECT (r).* FROM (SELECT (SELECT b AS r FROM topn b WHERE b.key=a.key ORDER BY value DESC LIMIT 1 OFFSET 3) AS r FROM topkeys a) foo;

Темп: 16 860 мс

INSERT INTO top SELECT (r).* FROM (SELECT (SELECT b AS r FROM topn b WHERE b.key=a.key ORDER BY value DESC LIMIT 1 OFFSET 4) AS r FROM topkeys a) foo;

Темп: 17 651 мс

INSERT INTO top SELECT (r).* FROM (SELECT (SELECT b AS r FROM topn b WHERE b.key=a.key ORDER BY value DESC LIMIT 1 OFFSET 5) AS r FROM topkeys a) foo;

Темп: 19 216 мс

SELECT * FROM top ORDER BY key,value;

Как вы можете видеть, вычисление top-n выполняется очень быстро (при условии, что n мало), но создание (обязательного) индекса происходит очень медленно, поскольку оно включает полную сортировку.

Лучше всего использовать формат, который быстро анализируется (либо двоичный, либо написать собственный агрегат C для вашей базы данных, что было бы лучшим выбором, ИМХО). Время выполнения в программе C не должно превышать 1 с, если python может сделать это за 1 с.

person bobflux    schedule 27.09.2009
comment
Да, прошлой ночью я читал статью Клингера 1990 года (Как точно читать числа с плавающей запятой). Я думаю, что я в безопасности, пока я придерживаюсь отправки чисел из 14 цифр и менее по быстрому пути и отталкиваю твердые числа на strtod. :) И это все еще дает мне ускорение в 4–6 раз в обычном случае. Ваш коррелированный подзапрос очень умен! Я все еще убежден, что эту задачу можно решить (даже правильно) значительно быстрее, чем даже программа OCaml, но я не знаю, закончу ли я проверку этой гипотезы, прежде чем мне придется заняться другими делами. - person Kragen Javier Sitaker; 28.09.2009
comment
Коррелированный подзапрос умен, если очень сильно закрыть глаза, не глядя на время создания индекса. Но тогда сортировка ГБ данных никогда не будет мгновенной. Если у вас уже есть индекс, хорошо для вас. - person bobflux; 28.09.2009
comment
В любом случае, если вы действительно хотите использовать текстовые данные (вы, потому что вам также нужно будет генерировать эти числа с плавающей запятой в тексте, что также очень медленно), то почему вы вообще анализируете числа с плавающей запятой ??? ? a = X.XXXXXe+E b = Y.YYYYYe+F (a ‹ b) эквивалентно E‹F или strcmp(X.XXXXX, Y.YYYYY) ‹ 0 Просто напечатайте свои числа в экспоненциальном представлении и используйте сравнение строк. Или, черт возьми, не используйте числа FP, если известен максимальный/минимальный диапазон, используйте целые числа (фиксированная точка) с ведущими нулями и сравнивайте их как строки. - person bobflux; 28.09.2009