Подтвердить что ты не робот

Что я могу сделать, чтобы ускорить этот код?

Я пытаюсь изучить Java, Scala и Clojure.

Я работаю над проблемами Project Euler на трех языках. Ниже приведен код проблемы № 5 (http://projecteuler.net/problem=5), а также время выполнения (в секундах) до сих пор по первым пяти проблемам. Мне поразительно, что версии Java и Clojure намного медленнее, чем Scala для проблемы № 5. Они работают на одной машине, то же самое jvm, и результаты согласуются в течение нескольких испытаний. Что я могу сделать, чтобы ускорить два (особенно версию Clojure)? Почему версия Scala намного быстрее?

Время работы (в секундах)

|---------|--------|--------|----------|
| problem | Java   | Scala  | Clojure  |
|=========|========|========|==========|
|    1    |  .0010 |  .1570 |   .0116  |
|    2    |  .0120 |  .0030 |   .0003  |
|    3    |  .0530 |  .0200 |   .1511  |
|    4    |  .2120 |  .2600 |   .8387  |
|    5    | 3.9680 |  .3020 | 33.8574  |

Версия Java версии №5

public class Problem005 {

  private static ArrayList<Integer> divisors;

  private static void initializeDivisors(int ceiling) {
    divisors = new ArrayList<Integer>();
    for (Integer i = 1; i <= ceiling; i++)
      divisors.add(i);
  }

  private static boolean isDivisibleByAll(int n) {
    for (int divisor : divisors)
      if (n % divisor != 0)
        return false;
    return true;
  }

  public static int findSmallestMultiple (int ceiling) {
    initializeDivisors(ceiling);
    int number = 1;
    while (!isDivisibleByAll(number))
      number++;
    return number;
  }

}

Scala Версия задачи № 5

object Problem005 {
  private def isDivisibleByAll(n: Int, top: Int): Boolean = 
    (1 to top).forall(n % _ == 0)

  def findSmallestMultiple(ceiling: Int): Int = {
    def iter(n: Int): Int = if (isDivisibleByAll(n, ceiling)) n else iter(n+1)
    iter(1)
  }

}

Clojure Версон проблемы № 5

(defn smallest-multiple-of-1-to-n
  [n]
  (loop [divisors (range 2 (inc n))
        i n]
    (if (every? #(= 0 (mod i %)) divisors)
      i
      (recur divisors (inc i)))))

ИЗМЕНИТЬ

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

Что касается первого вопроса, все три версии можно было бы ускорить, используя лучший алгоритм. В частности, создайте список наибольших общих факторов чисел 1-20 (2 ^ 4, 3 ^ 2, 5 ^ 1, 7 ^ 1, 11 ^ 1, 13 ^ 1, 17 ^ 1, 19 ^ 1) и умножьте их.

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

Для Java одно предложение заключалось в том, чтобы изменить ArrayList на примитивный массив из int. Это уменьшает время работы, сокращая примерно 0,5 - 1 секунду (я только что запустил это сегодня утром, и он сократил время работы с 4.386 секунд до 3.577 секунд. Это немного сократилось, но никто не смог придумать способ свести его до полутора секунд (аналогично версии Scala). Это удивительно, учитывая, что все три компилируются до байтового кода Java. и это увеличило время работы всего на 5 секунд.

Для Clojure, @mikera и @Webb дают несколько предложений, чтобы ускорить процесс. Они предлагают использовать loop/recur для быстрой итерации с двумя переменными цикла, unchecked-math для немного более быстрых математических операций (поскольку мы знаем, что здесь нет опасности переполнения), используйте примитивные длинные, а не бокс-номера, и избегайте функций более высокого порядка, таких как каждый?

Запустив код @mikera, я заканчиваю время работы 2,453 секунды, не так хорошо, как код Scala, но намного лучше, чем моя оригинальная версия и лучше, чем версия Java:

(set! *unchecked-math* true)

(defn euler5 
  []
  (loop [n 1 
         d 2]
    (if (== 0 (unchecked-remainder-int n d))
      (if (>= d 20) n (recur n (inc d)))
      (recur (inc n) 2))))

(defn is-divisible-by-all?
  [number divisors]
  (= 0 (reduce + (map #(mod 2 %) divisors))))

Для Scala, @didierc утверждает, что объект диапазона от 1 до 20 не является фактически списком объектов, а скорее одним объектом. Очень круто. Таким образом, разница в производительности в Scala заключается в том, что мы перебираем один объект вместо списка/массива целых чисел 1-20.

Фактически, если я изменяю вспомогательную функцию в методе Scala из объекта диапазона в список (см. ниже), время работы версии Scala увеличивается с 0,302 секунды до 226,59 секунд.

private def isDivisibleByAll2(n: Int, top: Int): Boolean = {
    def divisors: List[Int] = List(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20)
    divisors.forall(n % _ == 0)
  }

Таким образом, похоже, что @didierc правильно определил преимущество Scala в этом случае. Было бы интересно узнать, как этот тип объекта может быть реализован в java и Clojure.

@didierc, чтобы улучшить код, создав класс ImmutableRange следующим образом:

import java.util.Iterator;
import java.lang.Iterable;

public class ImmutableRange implements Iterable<Integer> {

  class ImmutableRangeIterator implements Iterator<Integer> {
    private int counter, end, step;

    public ImmutableRangeIterator(int start_, int end_, int step_) {
      end = end_;
      step = step_;
      counter = start_;
    }

    public boolean hasNext(){
      if (step>0) return counter  <= end;
      else return counter >= end;
    }

    public Integer next(){
      int r = counter;
      counter+=step;
      return r;
    }

    public void remove(){
      throw new UnsupportedOperationException();
    }

  }

  private int start, end, step;

  public ImmutableRange(int start_, int end_, int step_){
    // fix-me: properly check for parameters consistency
    start = start_;
    end = end_;
    step = step_;
  }

  public Iterator<Integer> iterator(){
    return new ImmutableRangeIterator(start,end,step);
  }
}

не улучшило время работы. Версия java работала на моей машине на 5.097 секунд. Таким образом, в конце мы получаем удовлетворительный ответ о том, почему версия Scala работает лучше, мы понимаем, как повысить производительность версии Clojure, но недостающее будет заключаться в том, чтобы понять, как реализовать Scala неизменяемый объект диапазона в Java.

ЗАКЛЮЧИТЕЛЬНЫЕ МЫСЛИ

Как прокомментировали некоторые из них, самым эффективным способом улучшить время работы этого кода является использование лучшего алгоритма. Например, следующий код java вычисляет ответ менее чем за 1 миллисекунду, используя Сито Эратосфена и Пробный отдел:

/**
 * Smallest Multiple
 *
 * 2520 is the smallest number that can be divided by each of the numbers 
 * from 1 to 10 without any remainder. What is the smallest positive number
 * that is evenly divisible by all of the numbers from 1 to 20?
 *
 * User: Alexandros Bantis
 * Date: 1/29/13
 * Time: 7:06 PM
 */
public class Problem005 {

  final private static int CROSSED_OUT = 0;
  final private static int NOT_CROSSED_OUT = 1;

  private static int intPow(int base, int exponent) {
    int value = 1;
    for (int i = 0; i < exponent; i++)
      value *= base;
    return value;
  }

  /**
   * primesTo computes all primes numbers up to n using trial by 
   * division algorithm
   *
   * @param n designates primes should be in the range 2 ... n
   * @return int[] a sieve of all prime factors 
   *              (0=CROSSED_OUT, 1=NOT_CROSSED_OUT)
   */
  private static int[] primesTo(int n) {
    int ceiling = (int) Math.sqrt(n * 1.0) + 1;
    int[] sieve = new int[n+1];

    // set default values
    for (int i = 2; i <= n; i++)
      sieve[i] = NOT_CROSSED_OUT;

    // cross out sieve values
    for (int i = 2; i <= ceiling; i++)
      for (int j = 2; i*j <= n; j++)
        sieve[i*j] = CROSSED_OUT;
    return sieve;
  }


  /**
   * getPrimeExp computes a prime factorization of n
   *
   * @param n the number subject to prime factorization
   * @return int[] an array of exponents for prime factors of n
   *               thus 8 => (0^0, 1^0, 2^3, 3^0, 4^0, 5^0, 6^0, 7^0, 8^0)
   */
  public static int[] getPrimeExp(int n) {
    int[] factor = primesTo(n);
    int[] primePowAll = new int[n+1];

    // set prime_factor_exponent for all factor/exponent pairs
    for (int i = 2; i <= n; i++) {
      if (factor[i] != CROSSED_OUT) {
        while (true) {
          if (n % i == 0) {
          n /= i;
          primePowAll[i] += 1;
          } else {
            break;
          }
        }
      }
    }

    return primePowAll;
  }

  /**
   * findSmallestMultiple computes the smallest number evenly divisible 
   * by all numbers 1 to n
   *
   * @param n the top of the range
   * @return int evenly divisible by all numbers 1 to n
   */
  public static int findSmallestMultiple(int n) {
    int[] gcfAll = new int[n+1];

    // populate greatest common factor arrays
    int[] gcfThis = null;
    for (int i = 2; i <= n; i++) {
      gcfThis = getPrimeExp(i);
      for (int j = 2; j <= i; j++) {
        if (gcfThis[j] > 0 && gcfThis[j] > gcfAll[j]) {
          gcfAll[j] = gcfThis[j];
        }
      }
    }

    // multiply out gcf arrays
    int value = 1;
    for (int i = 2; i <= n; i++) {
      if (gcfAll[i] > 0)
        value *= intPow(i, gcfAll[i]);
    }
    return value;
  }
}
4b9b3361

Ответ 1

Здесь гораздо более быстрая версия в Clojure:

(set! *unchecked-math* true)

(defn euler5 []
  (loop [n 1 
         d 2)]
    (if (== 0 (unchecked-remainder-int n d))
      (if (>= d 20) n (recur n (inc d)))
      (recur (inc n) 2))))

(time (euler5))
=> "Elapsed time: 2438.761237 msecs"

то есть. это примерно такая же скорость, что и ваша версия Java.

Основные трюки:

  • используйте loop/recur для быстрой итерации с двумя переменными цикла
  • используйте unchecked-math для немного более быстрых операций математики (поскольку мы знаем, что здесь нет опасности переполнения)
  • используйте примитивные длинны, а не номера в коробке.
  • избегать функций более высокого порядка, таких как every? - они имеют более высокие накладные расходы, чем операции с низким уровнем

Очевидно, что если вы действительно заботитесь о скорости, вы бы выбрали лучший алгоритм: -)

Ответ 2

Scala быстрее, потому что другие решения создают явные коллекции без всякой причины. В Scala, 1 to top создает объект, который представляет числа от 1 до top, но не указывает их в любом месте. В Java вы явно создаете список - и намного быстрее создать один объект, чем массив из 20 (на самом деле 21 объект, так как ArrayList также является объектом) на каждой итерации.

(Обратите внимание, что ни одна из версий на самом деле не находится рядом с оптимальной. См. "наименьшее общее число", что делает Eastsun, не упоминая об этом.)

Ответ 3

Первое, что я заметил, вероятно, окажут некоторое влияние на скорость в версии Java, это то, что вы создаете ArrayList<Integer> вместо int[].

У Java есть функция с версии 5, которая будет автоматически конвертировать между Integer и int - вы выполняете итерацию по этому списку, рассматривая их как тип int в вычислениях и вычислениях в математике, что заставляет Java проводят много циклов преобразования двух типов. Замена вашего ArrayList<Integer> на int[], вероятно, повлияет на производительность.

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

Это не похоже на выбор алгоритма для его решения, так как стратегия выглядит одинаково во всех трех (я не знаком с Clojure или Scala, поэтому я мог бы отсутствовать при некоторой тонкой разнице). Возможно, Scala может внутренне оптимизировать этот конкретный цикл/алгоритм, давая гораздо более быстрые результаты?

Ответ 4

На моем больно медленном компьютере код Clojure занимает почти 10 минут, поэтому я нахожусь примерно на 20x медленнее на старых верных здесь.

user=> (time (smallest-multiple-of-1-to-n 20))
"Elapsed time: 561420.259 msecs"
232792560

Возможно, вы сможете сделать этот же алгоритм более сопоставимым с другими, избегая лени, используя подсказки типа/примитивы/непроверенные операции и т.д. Код Clojure - это примитивы бокса для анонимной функции и создания/реализации ленивых последовательность для range каждой итерации loop. Эти накладные расходы обычно незначительны, но здесь он зацикливается сотни миллионов раз. Следующий неидиоматический код дает 3-кратное ускорение.

(defn smallest-multiple-of-1-to-n [n]
  (loop [c (int n)] 
    (if 
      (loop [x (int 2)]
        (cond (pos? (unchecked-remainder-int c x)) false
              (>= x n) true
              :else (recur (inc x))))
      c (recur (inc c)))))

user=> (time (smallest-multiple-of-1-to-n 20))
"Elapsed time: 171921.80347 msecs"
232792560

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

(defn gcd [a b]
  (if (zero? b) a (recur b (mod a b))))

(defn lcm 
  ([a b] (* b (quot a (gcd a b))))
  ([a b & r] (reduce lcm (lcm a b) r)))

user=> (time (apply lcm (range 2 21)))
"Elapsed time: 0.268749 msecs"
232792560

Таким образом, даже на моей древней машине это на 1000 раз быстрее, чем любая реализация вашего алгоритма на вашей быстрой машине. Я заметил, что решение для gcd/lcm fold было опубликовано для Scala. Таким образом, было бы интересно сравнить скорости этих подобных алгоритмов.

Ответ 5

Следуйте своему алгоритму, clojure примерно в 10 раз медленнее, чем версия java.

Немного быстрее для версии clojure: 46555ms = > 23846ms

(defn smallest-multiple-of-1-to-n
 [n]
  (let [divisors (range 2 (inc n))]
   (loop [i n]
     (if (loop [d 2]
        (cond (> d n) true
              (not= 0 (mod i d)) false
              :else (recur (inc d))))
     i
     (recur (inc i))))))

Немного быстрее для версии Java: 3248ms = > 2757ms

private static int[] divisors;

private static void initializeDivisors(int ceiling) {
    divisors = new int[ceiling];

    for (Integer i = 1; i <= ceiling; i++)
        divisors[i - 1] = i;
}

Ответ 6

Прежде всего, если число делится, например, на 4, оно также делится на 2 (один из 4 факторов).

Итак, начиная с 1-20 вам нужно только проверить некоторые цифры, а не все из них.

Во-вторых, если вы можете разделить факторизацию чисел, это просто попросит вас получить самый низкий общий множитель (это другой способ приблизиться к этой проблеме). Фактически, вы могли бы, вероятно, сделать это с помощью ручки и бумаги, так как это всего лишь 1-20.

Алгоритм, с которым вы работаете, довольно наивен - он не использует информацию, которую проблема предоставляет вам в полной мере.

Ответ 7

Вот более эффективное решение в scala:

def smallestMultipe(n: Int): Int = {
  @scala.annotation.tailrec
  def gcd(x: Int, y: Int): Int = if(x == 0) y else gcd(y%x, x)
  (1 to n).foldLeft(1){ (x,y) => x/gcd(x,y)*y }
}

И я сомневаюсь, почему ваша версия scala проблемы 1 настолько неэффективна. Вот два возможных решения задачи 1 в scala:

Короткий:

(1 until 1000) filter (n => n%3 == 0 || n%5 == 0) sum

Более эффективный:

(1 until 1000).foldLeft(0){ (r,n) => if(n%3==0||n%5==0) r+n else r }

Ответ 8

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

Во-первых, в проблеме Эйлера 5 мы не просим проверять делимость на 1 на n: всего один на двадцать. Это сказало: во-вторых, решение должно быть кратным 38. В-третьих, простые числа должны быть проверены сначала, и все делители должны быть проверены в порядке убывания, чтобы сбой как можно скорее. В-четвертых, некоторые делители также обеспечивают другие делители, т.е. Если число делится на 18, оно также делится на 9, 6 и 3. Наконец, все числа делятся на 1.

Это решение в Clojure работает в течение незначительного времени в 410 мс на MacBook Pro i7:

;Euler 5 helper
(defn divisible-by-all [n]
  (let [divisors [19 17 13 11 20 18 16 15 14 12]
        maxidx (dec (count divisors))]
  (loop [idx 0]
     (let [result (zero? (mod n (nth divisors idx)))]
       (cond
          (and (= idx maxidx) (true? result)) true
          (false? result) false
          :else (recur (inc idx)))))))

;Euler 5 solution
(defn min-divisible-by-one-to-twenty []
  (loop[ x 38 ] ;this one can be set MUCH MUCH higher...
    (let [result (divisible-by-all x)]
       (if (true? result) x (recur (+ x 38))))))

user=>(time (min-divisible-by-one-to-twenty))
"Elapsed time: 410.06 msecs"

Ответ 9

Я считаю, что это самый быстрый чистый Java-код, который вы могли бы написать для этой проблемы и наивный алгоритм. Это быстрее, чем Scala.

public class Euler5 {
  public static void main(String[] args) {
    int test = 2520;
    int i;
    again: while (true) {
      test++;
      for (i = 20; i >1; i--) {
        if (test % i != 0)
          continue again;
      }
      break;
    }
    System.out.println(test);
  }
}

Несколько мелких деталей:

  • Мы можем начать тестирование на 2520, так как вопрос упоминал его как значение:)
  • Мне казалось, что мы потерпели бы неудачу быстрее на вершине диапазона, чем внизу - я имею в виду, сколько вещей делится на 19 против, скажем, 3?
  • Я использовал ярлык для оператора continue. Это в основном дешевый синтетический способ для reset цикла for и увеличения нашего тестового примера.