@palkokrut

Как объяснить разную скорость выполнения вложенных циклов в разных языках?

Добрый вечер!
Читал пост о том, как код на ассемблере работает быстрее си.
thedeemon.livejournal.com/49226.html
Там суть такова, что есть два int массива на 256 элементов:
Задачка очень простая: есть два массива 16-битных целых чисел, найти сумму квадратов разностей. В оригинале это были блоки 16х16, и проходить их надо было в двойном цикле.... Цикл такой прогоняю 10 млн раз и смотрю время, а также смотрю, что за код сгенерировался.

И мне стало интересно, сколько этот процесс займет на C и на Python.
Я накидал такой код на C:
int main(int argc, char **argv)
{
	int g = 0;
	int a_list[] = {256 рандомных чисел от 1 до 100};
	int b_list[] = {256 рандомных чисел от 1 до 100};
	
	for (g = 0; g < 1000000; g++)
		array_func(a_list, b_list);
	return 0;
}

void array_func(int arr1[], int arr2[])
{
	int sum = 0, j=0;
	int x = 0, y = 0;
	for (y = 0; y < 16; y++)
	{
		for (x = 0; x < 16; x++)
		{
			short v = arr1[j] - arr2[j];
			sum += v*v;
			j++;
		}
	}
}

Такой же код у меня был на Питоне:
def main_func(a_list, b_list):
    summ = 0 #инициализация суммы
    j = 0 #независимый счетчик от 0 до 256
    for y in range(0, 16):
        for x in range(0, 16):
            p = a_list[j] - b_list[j]
            summ += p * p
            j += 1

for g in range(0, 1000000):
    main_func(a_list, b_list)

Замерял время выполнения с помощью линуксовой команды time.
И выяснил следующие удивительные для меня вещи:
  1. Второй python отрабатывает миллион итераций быстрее третьего: 40 с против 52-55 с
  2. Perl (на нем я писал, правда, методом тыка) отрабатывает тот же миллион за две с половиной минуты
  3. Си отрабатывает десять миллионов за 11 с
  4. Если переписать всё в одну функцию (соответственно, три вложенных цикла) - python и си выполняются на пару мс быстрее, а Perl - на минуту быстрее(!?)

Я программировать только начинаю учиться, и у меня несколько вопросов:
  1. Почему третий питон работает в этом случае медленнее второго?
  2. Почему Perl работает так медленно и ведет себя так странно?
  3. И самое главное - почему этот же код на JavaScript в мозилле срабатывает за 6,8 секунд?? Как он оказался быстрее скомпилированного C ?


P.S. Код на JavaScript:
function main_func(a_list, b_list) {
    for (var g = 0; g < 10000000; g++) {
        var summ = 0;
        var j = 0;
        for (var y = 0; y < 16; y++) {
            for (var x = 0; x < 16; x++) {
                var p = a_list[j] - b_list[j];
                summ += p * p;
                j++;
            }
        }
    }
}
console.time('test');
main_func(a_list, b_list);
console.timeEnd('test');
  • Вопрос задан
  • 2347 просмотров
Решения вопроса 1
@nirvimel
Меня как-то не устроило то, как вы оценили производительность Python, поэтому я взялся чуть подправить ваш пример для демонстрации совершенно других результатов. Запуск моего примера кроме установки numpy (pip install numpy), потребует установку еще одной интересной библиотеки (pip install numba) с ее установкой могут быть связанны некоторые трудности на различных ОС (она зависит еще и от llvm), но, поверьте, оно того стоит, полученные цифры производительности должны вам понравиться.
Для демонстрации реальной скорости вычислений в моем примере миллион итераций это довольно мало, камень не успевает хорошо прогреться. Обратите внимание, я заменил миллион итераций на 100 миллионов, поэтому полученный результат надо разделить на 100 для сравнения с другими языками. Вот, собственно, сам код:
from numba import jit
import numpy


@jit
def inner_func(a_list, b_list):
    sum = 0
    j = 0
    for y in range(0, 16):
        for x in range(0, 16):
            p = a_list[j] - b_list[j]
            sum += p * p
            j += 1
    return sum


@jit
def outer_func(a_list, b_list):
    sum = 0
    for g in range(0, 100000000):  # 100 000 000 == 10^8 !!!
        sum += inner_func(a_list, b_list)
    return sum


def main():
    maxint = numpy.iinfo(numpy.intc).max
    a_list = numpy.random.randint(maxint, size=256)
    b_list = numpy.random.randint(maxint, size=256)
    sum = outer_func(a_list, b_list)
    print(sum)


if __name__ == '__main__':
    main()

Если вам удалось это запустить и полученные цифры вас впечатлили, то я бы попросил подправить в вашем вопросе ту часть, которая касается Python, для восстановления справедливости в отношении этого великолепного языка.
Ответ написан
Пригласить эксперта
Ответы на вопрос 3
Olej
@Olej
инженер, программист, преподаватель
Я программировать только начинаю учиться, и у меня несколько вопросов:

Вообще то, такие "измерения" во многом - бессмыслица.
Ваши результаты будут зависеть:
- от уровня оптимизации, установленного для компилятора...
- от используемого компилятора с одного и того же языка (Clang из расхваливаемого здесь LLVM будет хуже GCC)
- от того, насколько ваши массивы (особенно при большем их размере)будут попадать в процессорные кэши и какого уровня кэши, что зависит от способа размещения массивов, порядка дивжения по ним и др. (и разница в скорости может быть не 15%, а 4-5 раз и более)
- от числа ядер и возможностей языковой системы задействовать несколько процессоров в исполняющей системе...
- ... от времени восхода Солнца на вашей широте ;-)

Так что такие "измерения" достаточно бессмысленное занятие, и ним можно оценивать только порядки величин.
Любопытства ради см. Языки программирования: скорость, Сравнительное обозрение языков программирования.
Ответ написан
@VZVZ
Reverse-Engineer, Software Developer, Architect
Вообще в таких случаях все сводится к интерпретации (ну или исполнению машинного кода, если он машинный; по сути-то это та же интерпретация).
А именно, к двум факторам:

1) насколько хорошо оптимизирован алгоритм интерпретатора (алгоритм разбора кода, + не слишком ли много интерпретатор делает лишних операций, не связанных с выполнением кода, например, он может подгружать что-то там)
> Замерял время выполнения с помощью линуксовой команды time.
Это то есть вы смотрели не время выполнения алгоритма, а время выполнения программы в целом, от запуска до завершения? Ну там вообще куча причин может быть, интерпретатор не моментально запускается, и что он подгружает и делает при запуске, вообще одним авторам известно.

2) насколько минимизирован (и вообще оптимизирован для интерпретации) сам интерпретируемый код. Именно поэтому код в бинарном формате (машинный код, байт-код) потенциально быстрее, чем код в текстовом формате. Во-первых, бинарник тупо компактнее, скажем вместо слова function (которое занимает самый минимум 8 байт) может быть всего лишь 1-2 кракозябра. Во-вторых, бинарный формат - он строгий, всё там более однозначно и не бывает всей этой мути с пробелами, табами и пр., интерпретатору меньше приходится думать что же значит каждый байт вместе с байтами перед ним и байтами после него. Минимизация кода с помощью утилит помогает сделать его компактнее, но интерпретатор-то все равно проверяет на табы, пробелы и т.д., так что и минифицированный код в текстовом формате - все равно не очень быстр всегда.
Но в случае с Си, стандартные компиляторы по дефолту просто загаживают код всякой "дрянью", это отлично видно, если взять OllyDbg и сравнить хотя бы по длине с тем кодом, который дают компиляторы ассемблера.
Ответ написан
Foolleren
@Foolleren
Компас есть, копать не люблю...
вы забыли на си сделать многопоточное вычисление вот он и проиграл джаве,
по поводу библиотек, их и на си много,
а ещё разные языки по разному работают с массивами, си например болт забивает на проверку границ массива оставляя это на совести программиста, ещё очень много зависит от того как массивы расположены в памяти , выравнивания, я уже не говорю про некоторые читы типа разворачивания цикла, вот у вас длинна цикла известна заранее, компилятору развернуть такой цикл как нефиг делать - меньше условных переходов быстрее код
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы