如何在C ++中快速输入数百万个整数?

时间:2019-04-16 17:25:44

标签: c++ input

我正在执行有关C ++中堆栈的数据结构编程任务。

在此作业中,我应该读取很多整数(在最坏的情况下,我应该读取1,600,000个整数),最后输出一些字符串。

作为一名学生,我提交了cpp源文件,然后网站对我的源代码进行了评估和评分。我得到了100%,但我想做得更好。 此分配的时间限制为2秒,我的源代码的执行时间为128毫秒。但是,成绩最好的学生仅用52毫秒即可完成任务。所以我想知道如何使我的代码更快。

我的源代码主要包含三个部分:

  1. 使用cin从OnlineJudge系统中读取很多整数(最多1,600,000个整数)。
  2. 尝试找到解决方案并将其存储在char数组中。
  3. 使用cout输出char数组。

OnlineJudge告诉我代码的执行时间。第一部分花费100毫秒,第二部分花费20毫秒,而第三部分花费12毫秒。因此,如果我想使代码更快,则应该提高输入速度。

OnlineJudge的输入如下:

5 2
1 2 3 5 4

第一行是n和m的两个整数,第二行是由空格分隔的n个整数。限制为:1 <= n <= 1,600,000和0 <= m <= 1,600,000。 为了读取超过一百万个整数,我的代码是这样的:

#include <iostream>
using namespace std;
int main()
{
    std::ios::sync_with_stdio(false);
    cin.tie(NULL);
    int *exit = new int[1600000];
    cin>>n>>m;
    for (int i=0;i<n;++i)
        cin>>exit[i];
    return 0;
}

如果n小,则OnlineJudge表示执行时间为0毫秒。 如果n非常大,例如160万OnlineJudge说此代码需要100毫秒。如果我删除

std::ios::sync_with_stdio(false);
cin.tie(NULL);

然后代码花费424毫秒。但是,在此作业中必须读取整数,因此我很好奇顶尖学生如何在52毫秒内完成“ cin,查找解决方案,退出”。

您对提高输入速度有任何想法吗?

2019.4.17:有人建议使用vector或std :: from_chars,但在此分配中,这些被禁止。 如果我写

#include <vector>

#include <charconv>

#include <array>

然后OnlineJudge说“编译错误”。

有人建议使用scanf,我的代码如下:

for (int i=0;i<n;++i)
    scanf("%d", &exit[i]);

但是执行时间是120毫秒。顺便说一句,我认为scanf不比cin快Using scanf() in C++ programs is faster than using cin?

有人建议使用getline。我很少使用此功能,我的代码如下:

stringstream ss;
string temp;
getline(cin, temp);
ss<<temp;ss>>n;ss>>m;
ss.clear();temp.clear();
getline(cin, temp);ss<<temp;
for (int i=0;i<n;++i)
    ss>>exit[i];

执行时间也是120毫秒。

有人建议使用mmap。我以前从未听说过此功能。看来此功能仅在Unix中可用?但是我正在使用Visual Studio2010。我的代码是这样的:

#include <unistd.h>
#include <sys/mman.h>
    //to load 1,600,000 integers
    int *exit = static_cast<int*>(mmap(NULL,1600*getpagesize(),PROT_READ,MAP_ANON|MAP_SHARED,0,0));
    for (int i=0;i<n;++i)
        cin>>*(exit+i);

OnlineJudge说“运行时错误(信号11)”而不是“编译错误”,信号11表示“无效的内存引用”,该信号在发出无效的虚拟内存引用或分段错误时发送到进程,即执行细分违规时。我不知道我的mmap有什么问题。希望你能告诉我。

2019.4.22:谢谢您的帮助。现在我成功解决了这个问题。关键功能是mmap。代码如下:

#include <sys/mman.h>
    cin.tie(NULL);
    std::ios::sync_with_stdio(false);
    string temp;

    int n,m;
    int *exit = new int[1600000];

    const int input_size = 13000000;
    void *mmap_void = mmap(0,input_size,PROT_READ,MAP_PRIVATE,0,0);
    char *mmap_input = (char *)mmap_void;
    int r=0,s=0;
    while (mmap_input[s]<'0' || mmap_input[s]>'9') ++s;
    while (mmap_input[s]>='0' && mmap_input[s]<='9')
    { r=r*10+(mmap_input[s]-'0');++s; }
    n=r;r=0;
    while (mmap_input[s]<'0' || mmap_input[s]>'9') ++s;
    while (mmap_input[s]>='0' && mmap_input[s]<='9')
    { r=r*10+(mmap_input[s]-'0');++s; }
    m=r;r=0;
    while (mmap_input[s]<'0' || mmap_input[s]>'9') ++s;
    for (int i=0;i<n;++i)
    {
        while (mmap_input[s]>='0' && mmap_input[s]<='9')
        { r=r*10+(mmap_input[s]-'0');++s; }
        ++s;
        exit[i]=r;r=0;
    }

mmap的执行时间和将char转换为整数需要8毫秒。现在,此作业的总执行时间为40毫秒,比52毫秒要快。

2 个答案:

答案 0 :(得分:4)

一些想法:

  1. 使用std::scanf而不是std::istream读取整数。即使有std::ios::sync_with_stdio(false)调用,后者也会由于多种原因而变慢。
  2. 通过将文件映射到内存中来读取文件。
  3. 解析整数比scanfstrtol更快。

示例:

#include <cstdio>

int main() {
    int n, m, a[1600000];
    if(2 != std::scanf("%d %d", &n, &m))
        throw;
    for(int i = 0; i < n; ++i)
        if(1 != std::scanf("%d", a + i))
            throw;
}

您还可以展开scanf循环以在一个调用中读取多个整数。例如:

#include <cstdio>

constexpr int step = 64;
char const fmt[step * 3] =
    "%d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d "
    "%d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d "
    "%d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d "
    "%d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d"
    ;
void main() {
    int a[1600000];
    int n, m;
    if(2 != std::scanf("%d %d", &n, &m))
        throw;

    for(int i = 0; i < n; i += step) {
        int expected = step < n - i ? step : n - i;
        int* b = a + i;
        int read = scanf(fmt + 3 * (step - expected),
                         b + 0x00, b + 0x01, b + 0x02, b + 0x03, b + 0x04, b + 0x05, b + 0x06, b + 0x07,
                         b + 0x08, b + 0x09, b + 0x0a, b + 0x0b, b + 0x0c, b + 0x0d, b + 0x0e, b + 0x0f,
                         b + 0x10, b + 0x11, b + 0x12, b + 0x13, b + 0x14, b + 0x15, b + 0x16, b + 0x17,
                         b + 0x18, b + 0x19, b + 0x1a, b + 0x1b, b + 0x1c, b + 0x1d, b + 0x1e, b + 0x1f,
                         b + 0x20, b + 0x21, b + 0x22, b + 0x23, b + 0x24, b + 0x25, b + 0x26, b + 0x27,
                         b + 0x28, b + 0x29, b + 0x2a, b + 0x2b, b + 0x2c, b + 0x2d, b + 0x2e, b + 0x2f,
                         b + 0x30, b + 0x31, b + 0x32, b + 0x33, b + 0x34, b + 0x35, b + 0x36, b + 0x37,
                         b + 0x38, b + 0x39, b + 0x3a, b + 0x3b, b + 0x3c, b + 0x3d, b + 0x3e, b + 0x3f);
        if(read != expected)
            throw;
    }
}

另一种方法是手动解析整数(将文件映射到内存中将对此有所帮助,并且解析整数的算法比标准atoi/strtolFastware - Andrei Alexandrescu要快得多):

int main() {
    int n, m, a[1600000];
    if(2 != std::scanf("%d %d", &n, &m))
        throw;

    for(int i = 0; i < n; ++i) {
        int r = std::getchar();
        while(std::isspace(r))
            r = std::getchar();
        bool neg = false;
        if('-' == r) {
            neg = true;
            r = std::getchar();
        }
        r -= '0';
        for(;;) {
            int s = std::getchar();
            if(!std::isdigit(s))
                break;
            r = r * 10 + (s - '0');
        }
        a[i] = neg ? -r : r;
    }
}

另一种方法是将文件映射到内存中并更快地解析它:

#include <boost/iostreams/device/mapped_file.hpp>

inline int find_and_parse_int(char const*& begin, char const* end) {
    while(begin != end && std::isspace(*begin))
        ++begin;
    if(begin == end)
        throw;
    bool neg = *begin == '-';
    begin += neg;
    int r = 0;
    do {
        unsigned c = *begin - '0';
        if(c >= 10)
            break;
        r = r * 10 + static_cast<int>(c);
    } while(++begin != end);
    return neg ? -r : r;
}

void main() {
    boost::iostreams::mapped_file f("random-1600000.txt", boost::iostreams::mapped_file::readonly);
    char const* begin = f.const_data();
    char const* end = begin + f.size();
    int n = find_and_parse_int(begin, end);
    int m = find_and_parse_int(begin, end);

    int a[1600000];
    for(int i = 0; i < n; ++i)
        a[i] = find_and_parse_int(begin, end);
}

Benchmark source code

请注意,不同版本的编译器和标准库的结果可能会有很大差异:

  • CentOS版本6.10,g ++-6.3.0,Intel Core i7-4790 CPU @ 3.60GHz
---- Best times ----
seconds,    percent, method
0.167985515,  100.0, getchar
0.147258495,   87.7, scanf
0.137161991,   81.7, iostream
0.118859546,   70.8, scanf-multi
0.034033769,   20.3, mmap-parse-faster
  • Ubuntu 18.04.2 LTS,g ++-8.2.0,Intel Core i7-7700K CPU @ 4.20GHz
---- Best times ----
seconds,    percent, method
0.133155952,  100.0, iostream
0.102128208,   76.7, scanf
0.082469185,   61.9, scanf-multi
0.048661004,   36.5, getchar
0.025320109,   19.0, mmap-parse-faster

答案 1 :(得分:-1)

  我的源代码的

时间是128毫秒。但是,成绩最好的学生只用了52毫秒

要运行整个程序,这正在进入误差范围。在现代OS上设置进程需要花费一些时间,无论输入输入数据的内容如何,​​如果服务器是共享资源,任何资源争用都会出现问题。提交相同的确切代码有多少不同?

  

int * exit =新的int [1600000];

内存分配是有成本的。在高性能循环等中,通常会完全避免使用它们,尽管单个分配不太可能对整体产生重大影响。

  

OnlineJudge的输入如下:

5 2
1 2 3 5 4
     

第一行是n和m的两个整数,第二行是由空格分隔的n个整数。限制为:1 <= n <= 1,600,000和0 <= m <= 1,600,000。为了读取超过一百万个整数,我的代码是这样的:

我发现std::cin等可能很慢,在某些情况下,数字解析功能也会很慢。如果您可以一口气读完整行然后解析,那可能会更快。解析收益通常来自以不安全的方式解析,前提是您可以随意输入,例如

  • ''总是分隔符吗?看起来是这样,您可以特殊情况结​​尾。例如。将整个“行”读入缓冲区,然后将“ \ n”替换为“。”。
  • 是否知道位数?总是1,还是小于5的其他少量数字?
  • 数字是否始终在有效范围内?
  • 输入是否始终是有效数字,没有要检查的随机字符?
  • 是否有负数?

了解这些内容后,您可能会说:

/*1 or 2 digit int, space delimiter. Advance p number of consumed chars.*/
int parse_small_int(char **p)
{
    int v = (*p)p[0] - '0';
    char c2 = (*p)[1];
    if (c2 == ' ') // 1 digit
    {
        return v;
    }
    else // assume 2 digit
    {
        v *= 10;
        v += (c2 - '0')
        (*p) += 2;
    }
}
  

您对提高输入速度有任何想法吗?

输出也一样,您似乎没有显示代码,但是std :: cout可能同样慢。而且,如果您对数字和允许的输出格式有所了解,则可以轻松击败<<std::to_stringitoa等。

  • 前导零是否有效?如果是这样,则可以为最大允许值编写无条件格式化程序。
  • 对预先分配的缓冲区进行这种格式化,然后打印整行。

例如

// always write 2 chars to p
void format_int_2_digit(int i, char *p)
{
    p[0] = '0' + (i / 10);
    p[1] = '0' + (i % 10);
}

另一种可能性是绕过C ++甚至C库,尽管在您的作业中可能不允许这样做。

例如,在Linux上,您可以将readwrite函数与STDIN_FILENOSTDOUT_FILENO一起使用。我从未亲自将这些与CRT版本进行比较,但是也许会有明显的不同。在Windows上有 ReadConsoleWriteConsole等,先使用GetStdHandle,然后再使用ReadFileWriteFile等。我再也没有测量这些。