Возможное объяснение более быстрого выполнения двух программ на C ++ (со сравнением с Python)? - PullRequest
2 голосов
/ 31 марта 2019

Обновление: программы на C ++ (как показано ниже) были скомпилированы без дополнительных флагов, т.е. g++ program.cpp.Однако повышение уровня оптимизации не меняет того факта, что перебор работает быстрее, чем метод запоминания (0,1 секунды VS 1 секунда на моем компьютере).

Context

Я пытаюсь вычислить число (<1 миллион) с самой длинной <a href="https://en.wikipedia.org/wiki/Collatz_conjecture" rel="nofollow noreferrer"> последовательностью Коллатца .Я написал алгоритм грубой силы и сравнил его с предложенной оптимизированной программой (которая в основном использует мемоизацию).

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

Ниже приведены сравнения, которые я имею на своем компьютере (Macbook Air);времена в начале кода программы в комментариях.

C ++ (грубая сила)

/**
 * runs in 1 second
 */

#include <iostream>
#include <vector>

unsigned long long nextSequence(unsigned long long n)
{
  if (n % 2 == 0)
    return n / 2;
  else
  {
    return 3 * n + 1;
  }
}

int main()
{
  int max_counter = 0;
  unsigned long long result;
  for (size_t i = 1; i < 1000000; i++)
  {
    int counter = 1;
    unsigned long long n = i;
    while (n != 1)
    {
      n = nextSequence(n);
      counter++;
    }
    if (counter > max_counter)
    {
      max_counter = counter;
      result = i;
    }
  }

  std::cout << result << " has " << max_counter << " sequences." << std::endl;

  return 0;
}

C ++ (памятка)

/**
 * runs in 2-3 seconds 
 */

#include <iostream>
#include <unordered_map>

int countSequence(uint64_t n, std::unordered_map<uint64_t, uint64_t> &cache)
{
  if (cache.count(n) == 1)
    return cache[n];

  if (n % 2 == 0)
    cache[n] = 1 + countSequence(n / 2, cache);
  else
    cache[n] = 2 + countSequence((3 * n + 1) / 2, cache);

  return cache[n];
}

int main()
{
  uint64_t max_counter = 0;
  uint64_t result;
  std::unordered_map<uint64_t, uint64_t> cache;
  cache[1] = 1;
  for (uint64_t i = 500000; i < 1000000; i++)
  {
    if (countSequence(i, cache) > max_counter)
    {
      max_counter = countSequence(i, cache);
      result = i;
    }
  }

  std::cout << result << std::endl;

  return 0;
}

В Python памяткатехника действительно работает быстрее.

Python (запоминание)

# runs in 1.5 seconds

def countChain(n):
    if n in values:
        return values[n]
    if n % 2 == 0:
        values[n] = 1 + countChain(n / 2)
    else:
        values[n] = 2 + countChain((3 * n + 1) / 2)
    return values[n]


values = {1: 1}
longest_chain = 0
answer = -1

for number in range(500000, 1000000):
    if countChain(number) > longest_chain:
        longest_chain = countChain(number)
        answer = number

print(answer)

Python (перебор)

# runs in 30 seconds


def countChain(n):
    if n == 1:
        return 1
    if n % 2 == 0:
        return 1 + countChain(n / 2)
    return 2 + countChain((3 * n + 1) / 2)


longest_chain = 0
answer = -1

for number in range(1, 1000000):
    temp = countChain(number)
    if temp > longest_chain:
        longest_chain = temp
        answer = number

print(answer)

Ответы [ 2 ]

4 голосов
/ 31 марта 2019

Я понимаю, что ваш вопрос касается разницы между двумя вариантами C ++, а не между скопированным C ++ и интерпретируемым питоном. Решительный ответ потребует компиляции кода с включенными оптимизациями и профилирования его исполнения. И ясность относительно того, является ли цель компилятора 64 или 32 битами.

Но учитывая порядок величин между обеими версиями кода C ++, быстрая проверка уже показывает, что ваша памятка потребляет больше ресурсов, чем вы зарабатываете.

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

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

std::cout << "Bucket count: "<<cache.bucket_count()<<"/"<<cache.max_bucket_count()<<std::endl; 

Чтобы избежать накладных расходов, связанных с этим, вы можете заранее распределить количество сегментов при построении:

std::unordered_map<uint64_t, uint64_t> cache(3000000);

Выполнение этого на ideone для небольшого и неформального теста позволило сэкономить почти 50% производительности.

Но тем не менее ... Хранение и поиск объектов в unordered_map требует вычисления хеш-кодов, составленных из множества арифметических операций. Поэтому я предполагаю, что эти операции просто тяжелее, чем вычисления методом грубой силы.

2 голосов
/ 31 марта 2019

Доступ к основной памяти значительно медленнее, чем вычисления, настолько, что, когда приходит время, вам нужно обрабатывать что-либо в течение очень небольшого (зависит от модели процессора) мегабайта, полученного из I / O или сетевое устройство.

Даже извлечение из L1 дороже по сравнению с целочисленными операциями.

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

Таким образом, люди считали операции с процессором и просто предполагали, что память может более или менее соответствовать.

В наше время это просто ... не может. Штраф за промах кэша ЦП составляет сотен целочисленных операций, и ваша хэш-карта с входом в миллион 16 байт в значительной степени гарантированно уничтожит не только кэши памяти процессора, но и TLB, который принимает отсрочить наказание от болезненного до разрушительного.

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