Как стать автором
Обновить

Комментарии 33

Если очень нужен питон и по другому ни как, то конечно можно.. но вообще дичь, просто неиспользуйте питон там где не надо.

А то как в известной мысли про молоток и гвозди.

Вы точно читали статью?

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

С чего бы для всех?

С go пропорции рутин и тредов вообще мало кого волнуют, а тех кого волнует не редко пишут на c++/rust.

Проблем с определением количества ресурсов нет, с cgroups тоже, пример: go, jvm, c# и куча прочих.

А если вы про сишную либу под капотом.. ну давайте поговрим о трединге/асинке в php? Там тоже ведь сишные либы подкапотом...

С go пропорции рутин и тредов вообще мало кого волнуют


В Golang по тем же критериям можно выбирать значение для GOMAXPROCS.

Размер ThreadPool в джаве и С++ тоже нужно как-то устанавливать. Думаете там всё по другому? И они умеют найти нужное количество процессоров изнутри докера?

А если вы про сишную либу под капотом

Если эти либы использовать эффективно, то есть один раз положить туда дынные и один раз достать то скорость будет близка к скорости самой либы. Вне зависимости от языка который это всё оркестрирует. Так всё машинное обучение на Питоне работает.

В любой задаче, где вам нужно будет перемалывать числа, вам придётся подбирать количество процессоров под ваш кейс. Чем более эффективен язык, тем с большей погрешностью это можно делать.

GOMAXPROCS контролирует количество операционных системных потоков (OS threads), которые могут исполнять код Go одновременно. Играть с этим параметром можно и нужно и вроде как "по феншую" надо ставить num CPU == GOMAXPROCS, но можно ставить больше что приведет к тому что утилизация на ядро станет выше.

Я что хочу сказать, сама фраза "по тем же критериям можно выбирать значение для GOMAXPROCS" навеивает какую-то сложную задачу, которая совсем и не задача. Никакой там сложной магии нет, повышаешь – тестишь. А там уже крутишься от того что тебе надо: Утилизация или Скорость. И по личному опыту (на минуточку у меня приложение в жало утилизирует 40 ядер) второе начинает падать только тогда, когда первое перескакивает значение 80-85%...
Т.е если программа 10% утилизировала и производила 1000 вычислений / за момент времени, то на 80% это будет 8000. А вот дальнейшее повышение начинает немного замедлять прибавку к скорости.

В любом случае подбор GOMAXPROCS было потрачено примерно 5 минут времени, методом увеличения для достижения оптимальных значений.

//Если эти либы использовать эффективно, то есть один раз положить туда дынные и один раз достать то скорость будет близка к скорости самой либы .

Нет, не будет. Давайте сначала определим что такое "близкая скорость". Вот в моих реалиях это наносекунды и лишняя 1000 – провал. Вы про какие близкие значения говорите? Давайте я вам просто ссылку дам и вы посмотрите:

https://benchmarksgame-team.pages.debian.net/benchmarksgame/fastest/gpp-python3.html

Смотрим на графу cpu secs и больше вопросов я думаю не будет. На самом деле замечательный ресурс с тестами. Наглядно показывает какие языки и в чем проигрывают.

В любом случае подбор GOMAXPROCS было потрачено примерно 5 минут
времени, методом увеличения для достижения оптимальных значений.

Так в общем статья как раз об этом подборе :)

Просто с лирическими отступлениями про то что не все CPU одинаковые. Ну и примеры на Питоне.

Нет, не будет. Давайте сначала определим что такое "близкая скорость".
Вот в моих реалиях это наносекунды и лишняя 1000 – провал.

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

не знаю, не встречал такого чтоб к примеру на TensorFlow данные лились через Python без обработки. Нафига он (Питон) тогда там нужен? Да и вообще нафига он нужен тогда при таких вводных как язык?

Я могу ошибаться, но в этом примере(https://www.tensorflow.org/tutorials/quickstart/beginner), данные не ковенрируется в питоновские объекты, а сразу загружаются в numpy объекты (обертака над нативным С массивом).

https://github.com/keras-team/keras/blob/v2.14.0/keras/datasets/mnist.py#L25-L86

> Нафига он (Питон) тогда там нужен?

Рулить процессом. И исследовать: посмотреть на срезы, собрать статистику, проверить гипотизы.

Захотелось мне однажды найти все счастливые ip адреса. Написал на питоне программу которая перебирала 4млрд адресов и считала суммы чисел внутри адреса.

Запустил на одном ядре, понял что не дождусь.

Запустил на всех ядрах, понял что не дождусь.

Попросил чатгпт переписать на си. Сишная версия отработала на одном ядре за 5 секунд.

А что такое «счастливые ip-адреса»? ) сумма первых шести и последних шести совпадает?

это так же как астрология.

на авито вон продают купюры со "счастливыми" номерами типа 57034723, видимо они счастливые для того кто их продаст.

Да что то типа того. Увидел у одного хостера предложение получить счастливый адрес и захотелось узнать а сколько их вообще.

Количество счастливых билетов можно получить без перебора. Тут, думаю, можно применить те же методы.

Вы меня сподвигли на эксперименты.

Наивнейшая реализация
octet = list(range(1,255))

counter = 0

for i1 in octet:
    print (f"{i1}", end='\r')
    for i2 in octet:
        for i3 in octet:
            for i4 in octet:
                if i1+i2 == i3+i4:
                    counter += 1

print (counter)

Заняла 4 минуты 13 секунд. Попробовал использовать модуль array - время совсем не изменилось. Удивился, думал хоть немного спасёт.

Переписываем, чуть-чуть включив мозги.

Вариант с мозгами
from collections import defaultdict 
from timeit import default_timer as timer

start_t = timer()

octet = list(range(1,255))

variantos = defaultdict(int)
for i1 in octet:
    print (f"{i1}", end='\r')
    for i2 in octet:
        summ = i1+ i2
        variantos[summ] += 1

counter = 0
for k, v in variantos.items():
    counter += v*v
        
print (counter)
end_t = timer()
print(end_t - start_t)

Причём первый цикл наверное не нужен - я просто не уверен, что список возможных сумм непрерывный, а проверять влом.

Получилось 0.012 секунд. Ой.

Результат, кстати, 10924794

Как насчет такой тупой реализации, обернутой в нумбу?

Тупой вариант с нумбой
import numba as nb
import numpy as np

from timeit import default_timer as timer

start_t = timer()

@nb.njit
def fast_counter():
    octet = np.arange(1, 255, dtype=np.uint8)
    counter = 0
    for i1 in octet:
        #print (f"{i1}", end='\r')
        for i2 in octet:
            for i3 in octet:
                for i4 in octet:
                    if i1+i2 == i3+i4:
                        counter += 1
    return counter

fast_counter()
end_t = timer()
print(end_t - start_t)

0.7611 секунд. И ещё ждать, пока нумба установится...

0.6482 секунд, если заменить dtype=np.uint8 на dtype=np.int32

0.5804 секунд, если не использовать numpy и вернуть list как было.

Это еще маленткие интеджеры закэшированны и не создаются в памяти.

А так итерация по np массиву это unbooxing и создания нового объекта. Не удивительно что список быстрее.

Вы считаете сумму октетов, а не сумму цифр в октетах, это заметно быстрее.

Не так уж и заметно, оказывается. Если рассчитать сумму цифр возможных значений октета заранее, то выходит 4:28 на моей машине.

Hidden text
sum_of_digits = [sum(int(x) for x in str(number)) for number in range(256)]
counter = 0
octet = tuple(range(0, 256))
for i1 in range(1, 256):
    print (f"{i1}", end='\n')
    for i2 in octet:
        for i3 in octet:
            for i4 in octet:
                if sum_of_digits[i1] + sum_of_digits[i2] == sum_of_digits[i3] + sum_of_digits[i4]:
                    counter += 1
print(counter)

Если переписать на C, то получается (на более слабой машине)

real    0m9.139s
user    0m9.133s
sys     0m0.005s

Ну если еще чуть подумать, то variations можно еще быстрее собрать (О(n) вместо О(n^2)).

Хотя если еще подумать, то и собирать variations не зачем там вообще один цикл на самом деле нужен:

l = 256
t = 0
for s in range(1, l):
    t += s*s * 2
t += l*l
print(t)

Да, и ответ у вас неверный. Вы почему-то считаете, что октет может иметь значения от 1 до 254 (включительно) хотя он от 0 до 255. И хотя некоторые счастливые IP v4 и зарезервированы как служебные и никто их выдать не может, но абстрагируюясь от этого ответ будет 11184896. Зарезервированные счастливые можно при желании вычесть.

Варианты: ваш "умный", мой с O(n) при построении variations и тот что выше:
11184896
0.010685138004191685
11184896
0.00014450500020757318
11184896
4.1819999751169235e-05

Наивнейшую реализацию можно оптимизировать спрятав итерацию в generator_expression.

Для вложенных циклов уже есть сахар https://docs.python.org/3/library/itertools.html#itertools.product

# v1
counter = sum(
  1 for i1 in octet
      for i2 in octet
          for i3 in octet
              for i4 in octet
                  if i1+i2 == i3+i4
)

# v2
counter = sum(
1 for i in product(octet, octet, octet, octet) if i[0] + i[1] == i[2] + i[3]
)

Питоняче будет вместо 1 написать то, что в if, а сам if убрать )

Вам интересно, почему быстрая функция намного быстрее? Возможно, стоит прочитать книгу об оптимизации низкоуровневого кода, над которой я работаю.



Два раза заполнить массив и один раз заполнить массив. Что здесь низкоуровневого?

Может сначала надо научиться пользоваться оптимизациями numpy?

Зачем вам умный контейнер для быстрых вычислений над элементами, если вы делаете всё снаружи?


Если вам надо сделать один массив из другого, да еще и одинаковой размерности, то надо все делать внутри.

from timeit import timeit

import numpy as np

def f_bad(img, noise_threshold=5):
    result = np.empty(arr.shape, arr.dtype)
    for i in range(result.shape[0]):
        result[i] = 0 if img[i] < noise_threshold else img[i]
    return result

def f_good(img, noise_threshold=5):
    return np.where(img < noise_threshold, 0, arr)


if __name__ == "__main__":
    arr = np.array(list(range(100)))
    print(timeit("f_bad(arr)", globals=globals())) # 15.4994
    print(timeit("f_good(arr)", globals=globals())) # 1.2924

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

Распараллеливание CPU-bound задач сводится к их непосредственному запуску процессов на разных ядрах CPU. Потоки, в данном случае, не подходят в принципе, т.к. процесс исполняется на одном ядре, и сколько потоков не выделяй -- они все будут исполняться в рамках одного.

Конкретно Python располагает не малым арсеналом встроенных механизмов распараллеливания задач на разных ядрах. Так, например, могу порекомендовать посмотреть в сторону ProcessPoolExecutor, т.к. он предоставляет весьма удобный интерфейс для управления пулом процессов на высоком уровне

Количество ядер, которое можно эффективно использовать, зависит от того кода, который напишете вы!

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

Прибавив ещё и то, что автор берет numpy и не знает, что это обёртка над C-библиотекой, которая спрашивает о выделении процессов у ОС, а не у CPython, и выходит, что статья написана в стиле "Я тут зашел в ваш Python и чет он ничего не может"

P.s.: считаю, что на хабре кроме редактуры на грамматику необходимо также ввести цензор на ошибочные или глупые статьи, а то пока мы имеем вот такие статьи, где автор искренне негодует по поводу того, что интерпретатор не может сам догадаться и исполнить его код так, как он хочет, а потоки почему-то не исполняются в рамках разных ядер

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

Эммм... Нет. В питоне многопоточность убита GIL'ом, но во-первых в данном примере его выключили, а во-вторых, в других языках GIL'а может и не быть вовсе. При этом использование потоков вместо процессов позволяет создавать более быстрые программы, за счёт отсутствия необходимости пересылать данные между адресными пространствами.

ProcessPoolExecutor из concurrent.futures или multiprocessing запускают сразу сколько угодно GIL-ов, создавая реальную многопоточность.

Ну т.к. самые сложные методы приходятся на сишный numpy, вопрос о CPU-bound уже можно пропустить.

Замеры были на 12700k /thread

ну и вообще, переводите ещё и комменты к оригинальной статье, там автору уже напихали в панамку за бенчмарк непонятно чего непонятно как

НЛО прилетело и опубликовало эту надпись здесь

А вот это вот отключения ГИЛ, насколько это безопасно? Насколько оправдано в реальных проектах? Не приведёт ли к разрушению стеков в случайный момент?

>>И наоборот, более быстрая функция может использовать преимущества не более чем девяти ядер; при увеличении их количества начинается замедление. Возможно, она упирается в какое-то другое узкое место, не в вычислительные ресурсы, например, в пропускную способность памяти.

Всё очень просто: полезли в регистры fpu - Hyper-threading  полетел, и вот у вас не 2 логических ядра а 1

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации