Увеличьте скорость при анализе числовых данных из больших файлов с помощью Python

Я читаю файл viewfactor, сгенерированный в Ansys (с использованием VFOPT), и преобразовываю его в 2d-массив в python. Я знаю, что окончательный фактор просмотра должен быть массивом 6982 * 6982.

Коэффициент просмотра (файл viewfactor.db, размером от нескольких МБ до нескольких ГБ) форматируется следующим образом:

Ansys Release 2020 R2          Build 20.2  Update 20200601  Format     0
RS3D

Number of Enclosures =        1

Enclosure Number =        1 Number of Surfaces =     6982
Element number =    28868 Face Number     2 TOTAL= 1.0000
  0.0000 0.0000 0.0000 0.0029 0.0056 0.0000 0.0000 0.0105 0.0000 0.0000
  0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000
  0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000
  (numbers numbers numbers)
  0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000
  0.0000 0.0000

Element number =    28869 Face Number     2 TOTAL= 1.0000
  0.0000 0.0000 0.0000 0.0029 0.0056 0.0000 0.0000 0.0105 0.0000 0.0000
  0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000
  etc etc

Итак, для 1-го элемента (элемент 28868) даны номера коэффициентов просмотра (по 10 за раз, с последней строкой, возможно, меньше), затем есть две строки, которые я могу игнорировать, затем даны новые числа и так далее.

В настоящее время я читаю файл следующим образом:

import numpy as np

# the file reading part here is rather quick, it takes a couple seconds
with open('viewfactor.db', 'r') as f:
    lines = f.read().splitlines()

nfaces = 6982 # I already had this number from before
vf = [[None] * nfaces] * nfaces
row = 0
col = 0

# this is the part that needs optimizing
for line in lines[7:]:
    if line == '': continue
    elif line.startswith('Element number'): # reset counters
        row += 1
        col = 0
    else: # save numbers
        nums = list(map(float, line.split()))
        vf[row][col:col+len(nums)] = nums
        col += len(nums)
vf = np.array(vf)

Это работает, но скрипт выполняется вечно. Я протестировал его с файлом viewfactor размером 350 МБ, и это заняло около 50 секунд (при необходимости я могу предоставить файл).

Можно ли уменьшить время выполнения? Могу ли я сотворить магию параллельных вычислений? Использовать С? Делать без петель?

Спасибо за любое предложение!

Проверьте это: numpy.org/doc/stable/reference/generated/numpy.fromstring.ht‌​ml

dankal444 08.04.2023 13:23

не могли бы вы поделиться ссылкой на этот файл?

RomanPerekhrest 08.04.2023 13:31

@ dankal444 Реализация Numpy не кажется особенно эффективной. На самом деле, это выглядит немного медленнее, чем np.array(list(map(float, line.split()))) на моей машине на нескольких простых тестах... Это довольно печально...

Jérôme Richard 08.04.2023 14:13

Такие вычисления можно значительно ускорить, используя нативные языки, такие как C или C++. Его также можно распараллелить с помощью OpenMP без особых усилий (в то время как это трудно сделать эффективно в Python, и полученный код, вероятно, будет значительно медленнее, чем код нативного кода). Разумно оптимизированный последовательный код C уже должен обеспечивать хорошее ускорение. Разбор — дорогостоящий процесс. Рассмотрите возможность использования двоичных файлов, если можете.

Jérôme Richard 08.04.2023 14:16

@JérômeRichard, к сожалению, ты прав. Я сделал numba-подход к байтам и нескольким строкам, кажется многообещающим (посмотрите на ответ)

dankal444 08.04.2023 17:35
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
5
79
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

У меня есть кое-что, что вы можете включить в свой код. Эта функция ниже принимает любую строку с числами (разделенную пробелом " " или символом новой строки "\n") и преобразует ее в массив numpy. Не кормите его отдельными строками, найдите все последовательные строки с номерами и дайте им это.

В моем бенчмарке преобразование 100 строк из 10 чисел было всего в 6 раз медленнее, чем выполнение одной строки вашего np.array(list(map(float, test_string.split()))). Так что ускорение должно быть не менее 10x

Примечание 1: вам нужно сначала преобразовать эти строки в байты, как в приведенном мной примере. Может быть, вы можете оптимизировать и работать с байтами для начала.

Примечание 2: первый запуск этой функции будет медленным из-за компиляции, не удалось найти правильную подпись для массива байтов. Может быть, @JeromeRichard может помочь с этим :) (EDIT: добавлена ​​подпись и исправления, предложенные Джеромом в комментариях)

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

@nb.jit('(Bytes(uint8, 1, "C"),)', nopython=True)
def numba_str_multiline(txt):
    # zero digit code = 48
    # space code = 32
    # new line code = 10
    # dot code = 46
    n_spaces_and_new_lines = 0
    for i in range(len(txt)):
        char = txt[i]
        if char == 32 or char == 10:
            n_spaces_and_new_lines += 1
    result = np.zeros(n_spaces_and_new_lines)

    current_number = 0.0
    idx_of_current_number = 0
    before_dot = True
    divisor = 10
    for i in range(len(txt)):
        char = txt[i]
        if char == 32 or char == 10:
            result[idx_of_current_number] = current_number
            idx_of_current_number += 1
            divisor = 10
            current_number = 0.0
            before_dot = True  # for cases if there is no dot at all
        elif char == 46:
            before_dot = False
        else:
            if before_dot:
                current_number = current_number * 10.0 + (char - 48)
            else:
                current_number += (char - 48) / divisor
                divisor *= 10

    return result


test_string = "1.2345 678.123400 0.0000 0.0029 0.0056 0.0000 0.0000 0.0105 0.0000 0.0000"
test_string_multiline = """1.2345 678.123400 0.0000 0.0029 0.0056 0.0000 0.0000 0.0105 0.0000 0.0000
""" * 100
test_bytes_multiline = bytes(test_string_multiline, 'utf-8')
result = numba_str_multiline(test_bytes_multiline)

Вы можете использовать подпись @nb.njit('(Bytes(uint8, 1, "C"),)') для того, чтобы этот код скомпилировался охотно ;) . Примечание. Numba очень неэффективен для строк и не очень хорош (но хорош) для байтов. Код Numba быстрее, чем наивный подход Python, но он все еще неэффективен по сравнению с тем, что можно было бы сделать на C (конечно, в несколько раз быстрее с оптимизированным кодом, особенно если числа имеют фиксированный размер). На моей машине значительное время уходит на повторение txt и выделение result.

Jérôme Richard 08.04.2023 18:28

Вы можете использовать for i in range(len(txt)): char = txt[i] для более быстрой работы и (char - 48) вместо (char - 48.0). На моей машине это на 25-30% быстрее. Ключом к производительности здесь является использование инструкции SIMD, но это на самом деле невозможно в Numba (по крайней мере, это непросто и не приводит к простому коду). Например, AVX может вычислить 32 символа подряд. Большинство процессоров x86 могут выполнять от 2 до 3 основных целочисленных инструкций AVX за цикл. Для двойной точности AVX может вычислять 4 числа за инструкцию (а часто и 2 за цикл). Обычно этого достаточно для обработки файлов со скоростью >1 ГиБ/с.

Jérôme Richard 08.04.2023 18:34

В любом случае, это в 15-20 раз быстрее, чем наивная реализация Python на моей машине после оптимизации, и этого может быть достаточно для ОП.

Jérôme Richard 08.04.2023 18:37

Привет @dankal444, спасибо за ответ! Извините, если я еще не протестировал код, но я уезжаю на пасхальные каникулы, и у меня нет времени добраться до своего компьютера, но я попробую скрипт, как только смогу. У меня есть возможность напрямую экспортировать файл коэффициента просмотра в двоичной форме, я посмотрю, поможет ли это в дальнейшем повышении скорости. Еще раз спасибо!

man-teiv 09.04.2023 10:26

@JérômeRichard спасибо за подпись и эти исправления! У меня было подобное улучшение на моей машине. Кроме того, я пытался преобразовать в uint8, но это не дало никаких улучшений. Кажется, байты дают ту же производительность, что и массив uint8 numpy (что, я думаю, хорошо?).

dankal444 09.04.2023 14:48

@man-teiv Счастливой Пасхи, мужик :) не торопись

dankal444 09.04.2023 14:50

Я проверил эту функцию, и она отлично работает! Время чтения сократилось с 50 с до 2,5 с, поэтому производительность увеличилась примерно в 20 раз :) У меня только есть сомнение: я хочу пропустить первый цикл для определения n_spaces_and_new_lines, так как я уже знаю это число. Но я попытался изменить функцию с def numba_str_multiline(txt): на def numba_str_multiline(txt, num):, чтобы напрямую построить массив как result = np.zeros(num), но появляется следующее сообщение об ошибке: TypeError: Failed in nopython mode pipeline (step: fix up args). Можно ли передать более одного аргумента?

man-teiv 09.04.2023 18:51

Да, можно передать любое (вменяемое) количество аргументов, нужно только поправить подпись. @nb.jit('(Bytes(uint8, 1, "C"), int64)', nopython=True)

dankal444 10.04.2023 10:21

Вот оно! Еще раз спасибо за ваше терпение и ваше супер полезное решение

man-teiv 11.04.2023 10:51

Другие вопросы по теме