Могу ли я сделать этот код на С++ быстрее, не делая его намного сложнее?

вот проблема, которую я решил с веб-сайта проблемы программирования (codechef.com, если кто-то не хочет видеть это решение, прежде чем попробовать себя). Это позволило решить проблему примерно за 5,43 секунды с помощью тестовых данных, другие решили эту же проблему с теми же данными теста за 0,14 секунды, но с гораздо более сложным кодом. Может ли кто-нибудь указать на определенные области моего кода, где я теряю производительность? Я все еще изучаю С++, поэтому я знаю, что есть миллион способов решить эту проблему, но я хотел бы знать, могу ли я улучшить собственное решение с некоторыми незначительными изменениями, а не переписывать все это. Или, если есть относительно простые решения, которые сопоставимы по длине, но будут работать лучше, чем мои, мне было бы интересно увидеть их также.

Пожалуйста, имейте в виду, что я изучаю С++, поэтому моя цель - улучшить код, который я понимаю, а не просто дать идеальное решение.

Спасибо

Проблема:

Цель этой проблемы - проверить, достаточно ли метод, который вы используете для чтения входных данных, для обработки проблем с фирменным знаком с огромным предупреждением ввода/вывода. Ожидается, что вы сможете обрабатывать не менее 2,5 МБ входных данных в секунду во время выполнения. Ограничение времени для обработки тестовых данных составляет 8 секунд.

Ввод начинается с двух натуральных чисел n k (n, k <= 10 ^ 7). Следующие n строк ввода содержат одно положительное целое число ti, не более 10 ^ 9, каждое. Выход

Напишите одно целое число для вывода, обозначая, сколько целых чисел ti делится на k. Пример

Ввод:

7 3
1
51
966369
7
9
999996
11

Вывод:

4

Решение:

#include <iostream>
#include <stdio.h>
using namespace std;

int main(){
  //n is number of integers to perform calculation on
  //k is the divisor
  //inputnum is the number to be divided by k
  //total is the total number of inputnums divisible by k

  int n,k,inputnum,total;

  //initialize total to zero
  total=0;

  //read in n and k from stdin
  scanf("%i%i",&n,&k);

  //loop n times and if k divides into n, increment total
  for (n; n>0; n--)
  {
    scanf("%i",&inputnum);
    if(inputnum % k==0) total += 1;
  }

 //output value of total
 printf("%i",total);
 return 0;
}

Ответ 1

Я проверил следующие строки ввода 28311552. Это в 10 раз быстрее, чем ваш код. То, что он делает, читает большой блок сразу, затем заканчивается до следующей новой строки. Цель здесь заключается в сокращении затрат ввода-вывода, поскольку scanf() считывает символ за раз. Даже при использовании stdio буфер, вероятно, слишком мал.

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

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

Вот тайминги (без оптимизатора мое решение только примерно в 6-7 раз быстрее, чем исходная ссылка)

[xavier:~/tmp] dalke% g++ -O3 my_solution.cpp
[xavier:~/tmp] dalke% time ./a.out < c.dat
15728647
0.284u 0.057s 0:00.39 84.6% 0+0k 0+1io 0pf+0w
[xavier:~/tmp] dalke% g++ -O3 your_solution.cpp
[xavier:~/tmp] dalke% time ./a.out < c.dat
15728647
3.585u 0.087s 0:03.72 98.3% 0+0k 0+0io 0pf+0w

Здесь код.

#include <iostream>
#include <stdio.h>
using namespace std;

const int BUFFER_SIZE=400000;
const int EXTRA=30;  // well over the size of an integer 

void read_to_newline(char *buffer) {
  int c;
  while (1) {
    c = getc_unlocked(stdin);
    if (c == '\n' || c == EOF) {
      *buffer = '\0';
      return;
    }
    *buffer++ = c;
  }
} 

int main() {
  char buffer[BUFFER_SIZE+EXTRA];
  char *end_buffer;
  char *startptr, *endptr;

  //n is number of integers to perform calculation on
  //k is the divisor
  //inputnum is the number to be divided by k
  //total is the total number of inputnums divisible by k

  int n,k,inputnum,total,nbytes;

  //initialize total to zero
  total=0;

  //read in n and k from stdin
  read_to_newline(buffer);
  sscanf(buffer, "%i%i",&n,&k);

  while (1) {
    // Read a large block of values
    // There should be one integer per line, with nothing else.
    // This might truncate an integer!
    nbytes = fread(buffer, 1, BUFFER_SIZE, stdin);
    if (nbytes == 0) {
      cerr << "Reached end of file too early" << endl;
      break;
    }
    // Make sure I read to the next newline.
    read_to_newline(buffer+nbytes);

    startptr = buffer;
    while (n>0) {
      inputnum = 0;
      // I had used strtol but that was too slow
      //   inputnum = strtol(startptr, &endptr, 10);
      // Instead, parse the integers myself.
      endptr = startptr;
      while (*endptr >= '0') {
        inputnum = inputnum * 10 + *endptr - '0';
        endptr++;
      }
      // *endptr might be a '\n' or '\0'

      // Might occur with the last field
      if (startptr == endptr) {
        break;
      }
      // skip the newline; go to the
      // first digit of the next number.
      if (*endptr == '\n') {
        endptr++;
      }
      // Test if this is a factor
      if (inputnum % k==0) total += 1;

      // Advance to the next number
      startptr = endptr;

      // Reduce the count by one
      n--;
    }
    // Either we are done, or we need new data
    if (n==0) {
      break;
    }
  }

 // output value of total
 printf("%i\n",total);
 return 0;
}

О, и он очень полагает, что входные данные находятся в правильном формате.

Ответ 2

Скорость не определяется вычислением - большая часть времени, затрачиваемого программой на выполнение, потребляется i/o.

Добавьте setvbuf вызовы перед первым scanf для значительного улучшения:

setvbuf(stdin, NULL, _IOFBF, 32768);
setvbuf(stdout, NULL, _IOFBF, 32768);

- изменить -

Предполагаемые магические числа - это новый размер буфера. По умолчанию FILE использует буфер размером 512 байт. Увеличение этого размера уменьшает количество раз, которое библиотека времени выполнения С++ должна выдавать вызов для чтения или записи в операционную систему, что, безусловно, является самой дорогостоящей операцией в вашем алгоритме.

Сохраняя размер буфера кратным 512, это устраняет фрагментацию буфера. Независимо от того, должен ли быть размер 1024*10 или 1024*1024, зависит от системы, для которой он предназначен. Для 16-разрядных систем размер буфера, превышающий 32 КБ или 64 КБ, обычно создает трудности при распределении буфера и, возможно, его управлении. Для любой более крупной системы сделайте ее максимально полезной - в зависимости от доступной памяти и от чего еще она будет конкурировать.

Отсутствие какой-либо известной конкуренции в области памяти, выберите размеры буферов примерно в размере связанных файлов. То есть, если входной файл равен 250 КБ, используйте его как размер буфера. При увеличении размера буфера уменьшается убывание. Для примера 250K буфер 100K потребует трех чтений, в то время как по умолчанию 512-байтовый буфер требует 500 считываний. Дальнейшее увеличение размера буфера, поэтому требуется только одно чтение, вряд ли сделает значительное улучшение производительности более чем на три чтения.

Ответ 3

попробуйте заменить if с помощью count += ((n%k)==0);. что может немного помочь.

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

Ответ 4

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

Хотя ваш пример настолько прост, что я почти не вижу того, что вы можете устранить, считая его частью вопроса для последующего чтения из stdin.

Несколько комментариев к коду: ваш пример не использует никаких потоков - не нужно включать заголовок iostream. Вы уже загружаете элементы библиотеки C в глобальное пространство имен, включая stdio.h вместо С++ версии заголовка cstdio, поэтому использование пространства имен не требуется.

Ответ 5

Вы можете прочитать каждую строку с помощью gets() и самостоятельно проанализировать строки без scanf(). (Обычно я бы не рекомендовал get(), но в этом случае ввод корректно задан.)

Пример программы C для решения этой проблемы:

#include <stdio.h>
int main() {
   int n,k,in,tot=0,i;
   char s[1024];
   gets(s);
   sscanf(s,"%d %d",&n,&k);
   while(n--) {
      gets(s);
      in=s[0]-'0';
      for(i=1; s[i]!=0; i++) {
        in=in*10 + s[i]-'0';   /* For each digit read, multiply the previous 
                                  value of in with 10 and add the current digit */
      }
      tot += in%k==0;          /* returns 1 if in%k is 0, 0 otherwise */
   }
   printf("%d\n",tot);
   return 0;
}

Эта программа примерно в 2,6 раза быстрее, чем решение, которое вы указали выше (на моей машине).

Ответ 6

Вы можете попытаться прочитать ввод строки за строкой и использовать atoi() для каждой строки ввода. Это должно быть немного быстрее, чем scanf, поскольку вы удаляете служебные данные "сканировать" в строке формата.

Ответ 7

Я думаю, что код в порядке. Я запустил его на своем компьютере менее чем за 0,3 секунды Я даже запускал его на гораздо больших входах менее чем за секунду.

Как вы его определяете?

Одна маленькая вещь, которую вы могли бы сделать, - удалить инструкцию if. начните с total = n, а затем внутри цикла:

total - = int ((input% k)/k + 1)//0, если делится, 1, если не

Ответ 8

Хотя я сомневаюсь, что CodeChef его примет, одна из возможностей - использовать несколько потоков, один для обработки ввода-вывода и другой для обработки данных. Это особенно эффективно для многоядерного процессора, но может помочь даже с одним ядром. Например, в Windows вы кодируете код, подобный этому (никакой реальной попытки соответствовать требованиям CodeChef - я сомневаюсь, что они согласятся с данными синхронизации на выходе):

#include <windows.h>
#include <process.h>
#include <iostream>
#include <time.h>
#include "queue.hpp"

namespace jvc = JVC_thread_queue;

struct buffer { 
    static const int initial_size = 1024 * 1024;
    char buf[initial_size];
    size_t size;

    buffer() : size(initial_size) {}
};

jvc::queue<buffer *> outputs;

void read(HANDLE file) {
    // read data from specified file, put into buffers for processing.
    //
    char temp[32];
    int temp_len = 0;
    int i;

    buffer *b;
    DWORD read;

    do { 
        b = new buffer;

        // If we have a partial line from the previous buffer, copy it into this one.
        if (temp_len != 0)
            memcpy(b->buf, temp, temp_len);

        // Then fill the buffer with data.
        ReadFile(file, b->buf+temp_len, b->size-temp_len, &read, NULL);

        // Look for partial line at end of buffer.
        for (i=read; b->buf[i] != '\n'; --i)
            ;

        // copy partial line to holding area.
        memcpy(temp, b->buf+i, temp_len=read-i);

        // adjust size.
        b->size = i;

        // put buffer into queue for processing thread.
        // transfers ownership.
        outputs.add(b);
    } while (read != 0);
}

// A simplified istrstream that can only read int's.
class num_reader { 
    buffer &b;
    char *pos;
    char *end;
public:
    num_reader(buffer *buf) : b(*buf), pos(b.buf), end(pos+b.size) {}

    num_reader &operator>>(int &value){ 
        int v = 0;

        // skip leading "stuff" up to the first digit.
        while ((pos < end) && !isdigit(*pos))
            ++pos;

        // read digits, create value from them.
        while ((pos < end) && isdigit(*pos)) {
            v = 10 * v + *pos-'0';
            ++pos;
        }
        value = v;
        return *this;
    }

    // return stream status -- only whether we're at end
    operator bool() { return pos < end; }
};

int result;

unsigned __stdcall processing_thread(void *) {
    int value;
    int n, k;
    int count = 0;

    // Read first buffer: n & k followed by values.
    buffer *b = outputs.pop();
    num_reader input(b);
    input >> n;
    input >> k;
    while (input >> value && ++count < n) 
        result += ((value %k ) == 0);

    // Ownership was transferred -- delete buffer when finished.
    delete b;

    // Then read subsequent buffers:
    while ((b=outputs.pop()) && (b->size != 0)) {
        num_reader input(b);
        while (input >> value && ++count < n)
            result += ((value %k) == 0);

        // Ownership was transferred -- delete buffer when finished.
        delete b;
    }
    return 0;
}

int main() { 
    HANDLE standard_input = GetStdHandle(STD_INPUT_HANDLE);
    HANDLE processor = (HANDLE)_beginthreadex(NULL, 0, processing_thread, NULL, 0, NULL);

    clock_t start = clock();
    read(standard_input);
    WaitForSingleObject(processor, INFINITE);
    clock_t finish = clock();

    std::cout << (float)(finish-start)/CLOCKS_PER_SEC << " Seconds.\n";
    std::cout << result;
    return 0;
}

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

#ifndef QUEUE_H_INCLUDED
#define QUEUE_H_INCLUDED

namespace JVC_thread_queue { 
template<class T, unsigned max = 256>
class queue { 
    HANDLE space_avail; // at least one slot empty
    HANDLE data_avail;  // at least one slot full
    CRITICAL_SECTION mutex; // protect buffer, in_pos, out_pos

    T buffer[max];
    long in_pos, out_pos;
public:
    queue() : in_pos(0), out_pos(0) { 
        space_avail = CreateSemaphore(NULL, max, max, NULL);
        data_avail = CreateSemaphore(NULL, 0, max, NULL);
        InitializeCriticalSection(&mutex);
    }

    void add(T data) { 
        WaitForSingleObject(space_avail, INFINITE);       
        EnterCriticalSection(&mutex);
        buffer[in_pos] = data;
        in_pos = (in_pos + 1) % max;
        LeaveCriticalSection(&mutex);
        ReleaseSemaphore(data_avail, 1, NULL);
    }

    T pop() { 
        WaitForSingleObject(data_avail,INFINITE);
        EnterCriticalSection(&mutex);
        T retval = buffer[out_pos];
        out_pos = (out_pos + 1) % max;
        LeaveCriticalSection(&mutex);
        ReleaseSemaphore(space_avail, 1, NULL);
        return retval;
    }

    ~queue() { 
        DeleteCriticalSection(&mutex);
        CloseHandle(data_avail);
        CloseHandle(space_avail);
    }
};
}

#endif

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

Ответ 9

2,5 МБ/с - 400 нс/байт.

Существует два больших процесса в байтах, ввод файлов и разбор.

Для ввода файла я просто загрузил его в большой буфер памяти. fread должен иметь возможность прочитать это при примерно полной ширине диска.

Для синтаксического анализа sscanf создается для общности, а не для скорости. atoi должен быть довольно быстрым. Моя привычка, к лучшему или худшему, состоит в том, чтобы сделать это сама, как в:

#define DIGIT(c)((c)>='0' && (c) <= '9')
bool parsInt(char* &p, int& num){
  while(*p && *p <= ' ') p++; // scan over whitespace
  if (!DIGIT(*p)) return false;
  num = 0;
  while(DIGIT(*p)){
    num = num * 10 + (*p++ - '0');
  }
  return true;
}

Петли, сначала над ведущими пробелами, затем над цифрами, должны быть почти такими же быстрыми, как машина может идти, конечно, намного меньше 400 нс/байт.

Ответ 10

Деление двух больших чисел тяжело. Возможно, улучшение должно было бы сначала охарактеризовать k немного, посмотрев на некоторые из меньших простых чисел. Скажем, 2, 3 и 5. Если k делится на любой из них, то inputnum также должен быть или inputnum не делится на k. Конечно, есть больше трюков для игры (вы можете использовать побитовое и inputnum для 1, чтобы определить, делитесь ли вы на 2), но я думаю, что просто удаление низких простых возможностей даст разумное повышение скорости (в любом случае это стоит того).