Self номера в C ++ - PullRequest
       13

Self номера в C ++

18 голосов
/ 13 января 2010

Эй, мои друзья и я пытаемся побить время выполнения друг друга для генерации " Self Numbers " от 1 до миллиона. Я написал свой на C ++ и все еще пытаюсь сбрить драгоценное время.

Вот что у меня есть,

#include <iostream>

using namespace std;

bool v[1000000];
int main(void) {
  long non_self = 0;
  for(long i = 1; i < 1000000; ++i) {
    if(!(v[i])) std::cout << i << '\n';
    non_self = i + (i%10) + (i/10)%10 + (i/100)%10 + (i/1000)%10 + (i/10000)%10 +(i/100000)%10;
    v[non_self] = 1;
  }
  std::cout << "1000000" << '\n';
  return 0;
}

Теперь код работает нормально, я просто хочу его оптимизировать. Какие-нибудь советы? Спасибо.

Ответы [ 15 ]

27 голосов
/ 13 января 2010

Я создал альтернативное решение C, которое не требует каких-либо операций по модулю или делению:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {
   int v[1100000];
   int j1, j2, j3, j4, j5, j6, s, n=0;
   memset(v, 0, sizeof(v));
   for (j6=0; j6<10; j6++) {
      for (j5=0; j5<10; j5++) {
         for (j4=0; j4<10; j4++) {
            for (j3=0; j3<10; j3++) {
               for (j2=0; j2<10; j2++) {
                  for (j1=0; j1<10; j1++) {
                     s = j6 + j5 + j4 + j3 + j2 + j1;
                     v[n + s] = 1;
                     n++;
                  }
               }
            }
         }
      }
   }
   for (n=1; n<=1000000; n++) {
      if (!v[n]) printf("%6d\n", n);
   }
}

Генерирует 97786 собственных номеров, включая 1 и 1000000.
С выходом это занимает

real        0m1.419s
user        0m0.060s
sys         0m0.152s

Когда я перенаправляю вывод в / dev / null, требуется

real     0m0.030s
user     0m0.024s
sys      0m0.004s

на моем четырехъядерном оборудовании с частотой 3 ГГц.

Для сравнения, ваша версия выдает одинаковое количество чисел, поэтому я предполагаю, что мы либо правы, либо в равной степени неверны; но твоя версия жует

real    0m0.064s
user    0m0.060s
sys     0m0.000s

при тех же условиях или примерно в 2 раза больше.

Это или тот факт, что вы используете long s, что не нужно на моей машине. Здесь int достигает 2 миллиардов. Может, стоит проверить INT_MAX на своем?

Update

У меня была догадка, что, может быть, лучше рассчитать сумму кусочно. Вот мой новый код:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {
   char v[1100000];
   int j1, j2, j3, j4, j5, j6, s, n=0;
   int s1, s2, s3, s4, s5;
   memset(v, 0, sizeof(v));
   for (j6=0; j6<10; j6++) {
      for (j5=0; j5<10; j5++) {
         s5 = j6 + j5;
         for (j4=0; j4<10; j4++) {
            s4 = s5 + j4;
            for (j3=0; j3<10; j3++) {
               s3 = s4 + j3;
               for (j2=0; j2<10; j2++) {
                  s2 = s3 + j2;
                  for (j1=0; j1<10; j1++) {
                     v[s2 + j1 + n++] = 1;
                  }
               }
            }
         }
      }
   }
   for (n=1; n<=1000000; n++) {
      if (!v[n]) printf("%d\n", n);
   }
}

... и что вы знаете, это уменьшило время для верхнего цикла с 12 мс до 4 мс. Или, может быть, 8, мои часы, кажется, немного нервничают.

Состояние дел, резюме

Фактическое нахождение собственных номеров до 1M теперь занимает примерно 4 мс, и у меня возникают проблемы с измерением любых дальнейших улучшений. С другой стороны, до тех пор, пока вывод выводится на консоль, он будет продолжать занимать около 1,4 секунд, несмотря на все мои усилия по использованию буферизации. Время ввода / вывода настолько сильно сокращает время вычисления, что любая дальнейшая оптимизация была бы по существу бесполезной. Таким образом, хотя я был вдохновлен дальнейшими комментариями, я решил оставить достаточно в покое.

Все приведенные примеры приведены на моей (довольно быстрой) машине и предназначены только для сравнения. Ваш пробег может отличаться.

13 голосов
/ 13 января 2010

Эти моды (%) выглядят дорого. Если вам разрешено перейти на базу 16 (или даже на базу 2), то вы, вероятно, сможете кодировать это намного быстрее. Если вам нужно остаться в десятичном формате, попробуйте создать массив цифр для каждого места (единицы, десятки, сотни) и создать некоторый код ролловера. Это значительно упростит суммирование.


В качестве альтернативы, вы можете узнать поведение основной функции self (давайте назовем ее s):

s = n + f(b,n)

где f(b,n) - сумма цифр числа n в базе b.

Для базы 10 ясно, что, поскольку цифры (также известные как наименее значимые) перемещаются из 0,1,2, ..., 9, что n и f(b,n) продолжаются в слежении, когда вы переходите из n до n+1, только в 10% случаев 9 переходит на 0, что не происходит, поэтому:

f(b,n+1) = f(b,n) + 1  // 90% of the time

Таким образом, ядро ​​собственной функции s продвигается как

n+1 + f(b,n+1) = n + 1 + f(b,n) + 1 = n + f(b,n) + 2

s(n+1) = s(n) + 2 // again, 90% of the time

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

Таким образом, первая оптимизация может удалить большую часть работы из 90% ваших циклов!

if ((n % 10) != 0) 
{
  n + f(b,n) = n-1 + f(b,n-1) + 2;
}

или

if ((n % 10) != 0)
{
  s = old_s + 2;
}

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

Если вы хотите больше, тогда разработайте простой алгоритм для изменения между итерациями для оставшихся 10%.

13 голосов
/ 13 января 2010

Сгенерируйте числа один раз, скопируйте вывод в ваш код в виде гигантской строки. Распечатать строку.

5 голосов
/ 13 января 2010

Если вы хотите, чтобы ваш вывод был быстрым, возможно, стоит попробовать заменить вывод iostream на обычный старый printf () - это зависит от правил победы в конкурсе, важно ли это.

3 голосов
/ 13 января 2010

Поскольку диапазон ограничен (от 1 до 1000000), максимальная сумма цифр не превышает 9 * 6 = 54. Это означает, что для реализации сита круговой буфер из 54 элементов должен быть идеально достаточно (и размер сита растет очень медленно по мере увеличения диапазона).

У вас уже есть решение на основе сита, но оно основано на предварительном построении полноразмерного буфера (сита из 1000000 элементов), который довольно неэлегатен (если не совсем неприемлем). Производительность вашего решения также страдает от нелокальности доступа к памяти.

Например, это возможная очень простая реализация

#define N 1000000U

void print_self_numbers(void)
{
  #define NMARKS 64U /* make it 64 just in case (and to make division work faster :) */

  unsigned char marks[NMARKS] = { 0 };
  unsigned i, imark;

  for (i = 1, imark = i; i <= N; ++i, imark = (imark + 1) % NMARKS)
  {
    unsigned digits, sum;

    if (!marks[imark])
      printf("%u ", i);
    else
      marks[imark] = 0;

    sum = i;
    for (digits = i; digits > 0; digits /= 10)
      sum += digits % 10;

    marks[sum % NMARKS] = 1;
  }
}

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

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

Что касается "оптимизаций" ... Нет смысла пытаться оптимизировать код, содержащий операции ввода-вывода. Вы не добьетесь ничего с помощью таких оптимизаций. Если вы хотите проанализировать производительность самого алгоритма, вам придется поместить сгенерированные числа в выходной массив и распечатать их позже.

3 голосов
/ 13 января 2010

cout или printf внутри цикла будут медленными. Если вы сможете удалить любые отпечатки из цикла, вы увидите значительное увеличение производительности.

3 голосов
/ 13 января 2010

Многопоточность (используйте разные массивы / диапазоны для каждого потока). Кроме того, не используйте больше потоков, чем количество ядер вашего процессора =)

1 голос
/ 29 января 2010

Я создал решение на основе CUDA, основанное на втором алгоритме Карла Смотрица. Сам код для идентификации Self Numbers очень быстрый - на моей машине он выполняется за ~ 45 наносекунд; это примерно в 150 раз быстрее, чем алгоритм Карла Смотрица, который работал на моей машине за 7 миллисекунд.

Однако существует узкое место, и похоже, что это интерфейс PCIe. Мой код потратил целых 43 миллисекунды, чтобы переместить вычисленные данные с графической карты обратно в RAM. Это может быть оптимизировано, и я посмотрю на это.

Тем не менее, 45 наноседонов чертовски быстро. Страшно быстро, на самом деле, и я добавил в свою программу код, который запускает алгоритм Карла Смотрица и сравнивает результаты на точность. Результаты точны. Вот вывод программы (скомпилирован в VS2008 64-bit, Windows7):

UPDATE

Я перекомпилировал этот код в режиме выпуска с полной оптимизацией и использованием статических библиотек времени выполнения, что дало значительные результаты. Оптимизатор, похоже, очень хорошо справился с алгоритмом Карла, сократив время выполнения с 7 мс до 1 мс. Реализация CUDA также ускорилась, с 35 до 20 человек. Копирование памяти с видеокарты в ОЗУ не было затронуто.

Вывод программы:

Running on device: 'Quadro NVS 295'
Reference Implementation Ran In 15603 ticks (7 ms)
Kernel Executed in 40 ms -- Breakdown:
  [kernel] : 35 us (0.09%)
  [memcpy] : 40 ms (99.91%)
CUDA Implementation Ran In 111889 ticks (51 ms)
Compute Slots: 1000448 (1954 blocks X 512 threads)
Number of Errors: 0

Код выглядит следующим образом:

файл: main.h

#pragma once

#include <cstdlib>
#include <functional>

typedef std::pair<int*, size_t> sized_ptr;
static sized_ptr make_sized_ptr(int* ptr, size_t size)
{
    return make_pair<int*, size_t>(ptr, size);
}

__host__ void ComputeSelfNumbers(sized_ptr hostMem, sized_ptr deviceMemory, unsigned const blocks, unsigned const threads);

inline std::string format_elapsed(double d) 
{
    char buf[256] = {0};

    if( d < 0.00000001 )
    {
        // show in ps with 4 digits
        sprintf(buf, "%0.4f ps", d * 1000000000000.0);
    }
    else if( d < 0.00001 )
    {
        // show in ns
        sprintf(buf, "%0.0f ns", d * 1000000000.0);
    }
    else if( d < 0.001 )
    {
        // show in us
        sprintf(buf, "%0.0f us", d * 1000000.0);
    }
    else if( d < 0.1 )
    {
        // show in ms
        sprintf(buf, "%0.0f ms", d * 1000.0);
    }
    else if( d <= 60.0 )
    {
        // show in seconds
        sprintf(buf, "%0.2f s", d);
    }
    else if( d < 3600.0 )
    {
        // show in min:sec
        sprintf(buf, "%01.0f:%02.2f", floor(d/60.0), fmod(d,60.0));
    }
    // show in h:min:sec
    else 
        sprintf(buf, "%01.0f:%02.0f:%02.2f", floor(d/3600.0), floor(fmod(d,3600.0)/60.0), fmod(d,60.0));

    return buf;
}

inline std::string format_pct(double d)
{
    char buf[256] = {0};
    sprintf(buf, "%.2f", 100.0 * d);
    return buf;
}

файл: main.cpp

#define _CRT_SECURE_NO_WARNINGS 

#include <windows.h>
#include "C:\CUDA\include\cuda_runtime.h"
#include <cstdlib>
#include <iostream>
#include <string>
using namespace std;
#include <cmath>
#include <map>
#include <algorithm>
#include <list>

#include "main.h"

int main()
{
    unsigned numVals = 1000000;
    int* gold = new int[numVals];
    memset(gold, 0, sizeof(int)*numVals);

    LARGE_INTEGER li = {0}, li2 = {0};
    QueryPerformanceFrequency(&li);
    __int64 freq = li.QuadPart;

    // get cuda properties...
    cudaDeviceProp cdp = {0};
    cudaError_t err = cudaGetDeviceProperties(&cdp, 0);
cout << "Running on device: '" << cdp.name << "'" << endl;

    // first run the reference implementation
    QueryPerformanceCounter(&li);
    for( int j6=0, n = 0; j6<10; j6++ ) 
    {
        for( int j5=0; j5<10; j5++ ) 
        {
            for( int j4=0; j4<10; j4++ ) 
            {
                for( int j3=0; j3<10; j3++ ) 
                {
                    for( int j2=0; j2<10; j2++ ) 
                    {
                        for( int j1=0; j1<10; j1++ )  
                        {
                            int s = j6 + j5 + j4 + j3 + j2 + j1;
                            gold[n + s] = 1;
                            n++;
                        }
                    }
                }
            }
        }
    }
    QueryPerformanceCounter(&li2);
    __int64 ticks = li2.QuadPart-li.QuadPart;
    cout << "Reference Implementation Ran In " << ticks << " ticks" << " (" << format_elapsed((double)ticks/(double)freq) << ")" << endl;

    // now run the cuda version...
    unsigned threads = cdp.maxThreadsPerBlock;
    unsigned blocks = numVals/threads;
    if( numVals%threads ) ++blocks;
    unsigned computeSlots = blocks * threads;   // this may be != the number of vals since we want 32-thread warps

    // allocate device memory for test
    int* deviceTest = 0;
    err = cudaMalloc(&deviceTest, sizeof(int)*computeSlots);
    err = cudaMemset(deviceTest, 0, sizeof(int)*computeSlots);

    int* hostTest = new int[numVals];   // the repository for the resulting data on the host
    memset(hostTest, 0, sizeof(int)*numVals);

    // run the CUDA code...
    LARGE_INTEGER li3 = {0}, li4={0};
    QueryPerformanceCounter(&li3);
    ComputeSelfNumbers(make_sized_ptr(hostTest, numVals), make_sized_ptr(deviceTest, computeSlots), blocks, threads);
    QueryPerformanceCounter(&li4);

    __int64 ticksCuda = li4.QuadPart-li3.QuadPart;
    cout << "CUDA Implementation Ran In " << ticksCuda << " ticks" << " (" << format_elapsed((double)ticksCuda/(double)freq) << ")" << endl;
    cout << "Compute Slots: " << computeSlots << " (" << blocks << " blocks X " << threads << " threads)" << endl;


    unsigned errorCount = 0;
    for( size_t i = 0; i < numVals; ++i )
    {
        if( gold[i] != hostTest[i] )
        {
            ++errorCount;
        }
    }

    cout << "Number of Errors: " << errorCount << endl;

    return 0;
}

файл: self.cu

#pragma warning( disable : 4231)
#include <windows.h>
#include <cstdlib>
#include <vector>
#include <iostream>
#include <string>
#include <iomanip>
using namespace std;
#include "main.h"

__global__ void SelfNum(int * slots)
{
    __shared__ int N;
    N = (blockIdx.x * blockDim.x) + threadIdx.x;

    const int numDigits = 10;

    __shared__ int digits[numDigits];
    for( int i = 0, temp = N; i < numDigits; ++i, temp /= 10 )
    {
        digits[numDigits-i-1] = temp - 10 * (temp/10)      /*temp % 10*/;
    }

    __shared__ int s;
    s = 0;
    for( int i = 0; i < numDigits; ++i )
        s += digits[i];

    slots[N+s] = 1;
}

__host__ void ComputeSelfNumbers(sized_ptr hostMem, sized_ptr deviceMem, const unsigned  blocks, const unsigned threads)
{
    LARGE_INTEGER li = {0};
    QueryPerformanceFrequency(&li);
    double freq = (double)li.QuadPart;

    LARGE_INTEGER liStart = {0};
    QueryPerformanceCounter(&liStart);

    // run the kernel
    SelfNum<<<blocks, threads>>>(deviceMem.first);
    LARGE_INTEGER liKernel = {0};
    QueryPerformanceCounter(&liKernel);

    cudaMemcpy(hostMem.first, deviceMem.first, hostMem.second*sizeof(int), cudaMemcpyDeviceToHost); // dont copy the overflow - just throw it away
    LARGE_INTEGER liMemcpy = {0};
    QueryPerformanceCounter(&liMemcpy);

    // display performance stats
    double e = double(liMemcpy.QuadPart - liStart.QuadPart)/freq,
        eKernel = double(liKernel.QuadPart - liStart.QuadPart)/freq,
        eMemcpy = double(liMemcpy.QuadPart - liKernel.QuadPart)/freq;

    double pKernel = eKernel/e,
        pMemcpy = eMemcpy/e;

    cout << "Kernel Executed in " << format_elapsed(e) << " -- Breakdown: " << endl
        << "  [kernel] : " << format_elapsed(eKernel) << " (" << format_pct(pKernel) << "%)" << endl
        << "  [memcpy] : " << format_elapsed(eMemcpy) << " (" << format_pct(pMemcpy) << "%)" << endl;



}

UPDATE2:

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

Вывод моего нового ядра:

Reference Implementation Ran In 69610 ticks (5 ms)
Kernel Executed in 2 ms -- Breakdown:
  [kernel] : 39 us (1.57%)
  [memcpy] : 2 ms (98.43%)
CUDA Implementation Ran In 62970 ticks (4 ms)
Compute Slots: 1000448 (1954 blocks X 512 threads)
Number of Errors: 0

Единственный код, который я изменил, - это само ядро, поэтому это все, что я опубликую здесь:

__global__ void SelfNum(int * slots)
{
    int N = (blockIdx.x * blockDim.x) + threadIdx.x;

    int s = 0;

    int temp = N;
    s += temp - 10 * (temp/10)      /*temp % 10*/;
    s += temp - 10 * ((temp/=10)/10)      /*temp % 10*/;
    s += temp - 10 * ((temp/=10)/10)      /*temp % 10*/;
    s += temp - 10 * ((temp/=10)/10)      /*temp % 10*/;
    s += temp - 10 * ((temp/=10)/10)      /*temp % 10*/;
    s += temp - 10 * ((temp/=10)/10)      /*temp % 10*/;
    s += temp - 10 * ((temp/=10)/10)      /*temp % 10*/;
    s += temp - 10 * ((temp/=10)/10)      /*temp % 10*/;
    s += temp - 10 * ((temp/=10)/10)      /*temp % 10*/;
    s += temp - 10 * ((temp/=10)/10)      /*temp % 10*/;

    slots[N+s] = 1;
}
1 голос
/ 13 января 2010

Это может помочь ускорить вывод Cost iostreams:

cin.tie(0);
ios::sync_with_stdio(false);

Поместите их в main, прежде чем начать писать cout.

1 голос
/ 13 января 2010

Почему бы вместо этого не использовать рекуррентное отношение, указанное на странице википедии? Это должно быть невероятно быстро.

РЕДАКТИРОВАТЬ: Игнорировать это ... отношение рекуррентности генерирует некоторые, но не все собственные числа. На самом деле их очень мало. Это не очень понятно со страницы Википедии: (

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