我可以更快地制作这个C ++代码而不会让它更复杂吗?

时间:2010-01-23 22:28:39

标签: c++ performance

这是我从编程问题网站(codechef.com)解决的问题,以防任何人在尝试自己之前不想看到这个解决方案。这使用测试数据在大约5.43秒内解决了问题,其他人在0.14秒内使用相同的测试数据解决了同样的问题,但代码复杂得多。任何人都可以指出我的代码中我失去性能的特定区域吗?我还在学习C ++所以我知道有一百万种方法可以解决这个问题,但是我想知道我是否可以通过一些微妙的改变来改进我自己的解决方案,而不是重写整个事情。或者,如果有任何相对简单的解决方案,其长度相当,但性能会比我的更好,我也有兴趣看到它们。

请记住我正在学习C ++,所以我的目标是改进我理解的代码,而不仅仅是给出一个完美的解决方案。

由于

问题:

此问题的目的是验证用于读取输入数据的方法是否足够快以处理带有巨大输入/输出警告的问题。您应该能够在运行时每秒处理至少2.5MB的输入数据。处理测试数据的时间限制为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;
}

10 个答案:

答案 0 :(得分:12)

速度不是由计算决定的 - 程序运行的大部分时间都是由i / o消耗的。

在第一个scanf之前添加setvbuf个来进行重大改进:

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

- 编辑 -

所谓的幻数是新的缓冲区大小。默认情况下,FILE使用512字节的缓冲区。增加此大小会减少C ++运行时库对操作系统发出读取或写入调用的次数,这是算法中最昂贵的操作。

通过将缓冲区大小保持为512的倍数,可以消除缓冲区碎片。大小应该是1024*10还是1024*1024取决于它要运行的系统。对于16位系统,大于32K或64K的缓冲区大小通常会导致分配缓冲区的困难,并且可能会对其进行管理。对于任何更大的系统,根据可用内存以及它将与之竞争的其他内容,使其尽可能大。

缺少任何已知的内存争用,请选择大小与关联文件大小相同的缓冲区大小。也就是说,如果输入文件是250K,则将其用作缓冲区大小。随着缓冲区大小的增加,肯定会有递减的回报。对于250K示例,100K缓冲区需要三次读取,而默认512字节缓冲区需要500次读取。进一步增加缓冲区大小,因此只需要一次读取就不可能在三次读取时显着提高性能。

答案 1 :(得分:6)

我在28311552行输入上测试了以下内容。它比你的代码快10倍。它的作用是立即读取一个大块,然后完成下一个换行符。这里的目标是降低I / O成本,因为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 :(得分:2)

尝试用count += ((n%k)==0);替换if语句。这可能有点帮助。

但我认为你真的需要将输入缓冲到临时数组中。从输入一次读取一个整数是昂贵的。如果您可以分离数据采集和数据处理,编译器可能能够生成用于数学运算的优化代码。

答案 3 :(得分:2)

I / O操作是瓶颈。尽可能地限制它们,例如在一个步骤中将所有数据加载到具有缓冲流的缓冲区或数组中。

虽然你的例子非常简单,但我几乎看不到你可以消除什么 - 假设这是从stdin进行后续阅读的问题的一部分。

对代码的一些评论:您的示例不使用任何流 - 不需要包含iostream标头。您已经通过包含stdio.h而不是标头cstdio的C ++版本将C库元素加载到全局命名空间,因此使用命名空间std不是必需的。

答案 4 :(得分:2)

您可以使用gets()读取每一行,并在不使用scanf()的情况下自行解析字符串。 (通常我不推荐使用gets(),但在这种情况下,输入是明确指定的。)

解决此问题的示例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倍。

答案 5 :(得分:1)

您可以尝试逐行读取输入,并为每个输入行使用atoi()。这应该比scanf快一点,因为你删除了格式字符串的“扫描”开销。

答案 6 :(得分:1)

我认为代码很好。我在不到0.3秒的电脑上运行它 我甚至在不到一秒的时间内在更大的输入上运行它。

你怎么计时?

你可以做的一件小事是删除if语句。 以total = n开始,然后在循环内部开始:

总计 - = int((输入%k)/ k + 1)// 0如果可分,则1如果不是

答案 7 :(得分:1)

虽然我怀疑CodeChef会接受它,但有一种可能性是使用多个线程,一个用于处理I / O,另一个用于处理数据。这在多核处理器上尤其有效,但即使使用单核也可以提供帮助。例如,在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

你从中获得多少收益取决于阅读所花费的时间与其他处理所花费的时间之间的关系。在这种情况下,其他处理是微不足道的,它可能不会获得太多。如果花费更多时间处理数据,多线程可能会获得更多。

答案 8 :(得分:1)

2.5mb / sec是400ns / byte。

有两个很大的每字节进程,文件输入和解析。

对于文件输入,我只是将其加载到一个大内存缓冲区中。 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;
}

循环,首先超过前导空白,然后是数字,应该几乎和机器一样快,当然要小于400ns / byte。

答案 9 :(得分:0)

划分两个大数字很难。也许改进是首先通过查看一些较小的素数来表征k。我们现在说2,3和5。如果k可被任何这些整除,则inputnum也需要或inputnum不能被k整除。当然还有更多的技巧可以玩(你可以使用bitwise和inputnum来确定你是否可以被2整除),但我认为只需删除低素数可能性就可以提高速度(无论如何都值得一试)。