Java 在文本文件中求整数和的最快方法 问题:
假设您有一个大的ASCII文本文件,每行上都有一个随机的非负整数,每个整数的范围从0到100000000。文件中有100000000行。读取文件并计算所有整数之和的最快方法是什么 约束条件:我们有10MB的RAM可供使用。该文件的大小为1GB,因此我们不希望读取整个文件,然后再对其进行处理 以下是我尝试过的各种解决方案。我发现结果相当令人惊讶 我错过了什么更快的吗 请注意:下面给出的所有计时均用于运行算法10次(运行一次并放弃;启动计时器;运行10次;停止计时器)。这台机器是一个相当慢的核心2二重唱 方法1:自然法 首先要尝试的是显而易见的方法:Java 在文本文件中求整数和的最快方法 问题:,java,performance,file-io,Java,Performance,File Io,假设您有一个大的ASCII文本文件,每行上都有一个随机的非负整数,每个整数的范围从0到100000000。文件中有100000000行。读取文件并计算所有整数之和的最快方法是什么 约束条件:我们有10MB的RAM可供使用。该文件的大小为1GB,因此我们不希望读取整个文件,然后再对其进行处理 以下是我尝试过的各种解决方案。我发现结果相当令人惊讶 我错过了什么更快的吗 请注意:下面给出的所有计时均用于运行算法10次(运行一次并放弃;启动计时器;运行10次;停止计时器)。这台机器是一个相当慢的核心2二
private long sumLineByLine() throws NumberFormatException, IOException {
BufferedReader br = new BufferedReader(new FileReader(file));
String line;
long total = 0;
while ((line = br.readLine()) != null) {
int k = Integer.parseInt(line);
total += k;
}
br.close();
return total;
}
请注意,最大可能的返回值是10^17,这仍然很容易适应长的
,因此我们不必担心溢出
在我的机器上,运行11次并打折第一次运行大约需要92.9秒
方法2:一个小的调整
受到对的一条评论的启发,我尝试不创建新的intk
来存储解析行的结果,而是直接将解析后的值添加到total
。因此:
while ((line = br.readLine()) != null) {
int k = Integer.parseInt(line);
total += k;
}
变成这样:
while ((line = br.readLine()) != null)
total += Integer.parseInt(line);
我确信这不会有任何区别,并且认为编译器很可能会为这两个版本生成相同的字节码。但是,令我惊讶的是,它确实缩短了一点时间:我们的时间降到了92.1秒
方法3:手动解析整数
到目前为止,让我感到困扰的是,我们将字符串
转换为int
,然后在末尾添加它。在我们前进的过程中添加,不是更快吗?如果我们自己解析字符串
会发生什么?像这样的
private long sumLineByLineManualParse() throws NumberFormatException,
IOException {
BufferedReader br = new BufferedReader(new FileReader(file));
String line;
long total = 0;
while ((line = br.readLine()) != null) {
char chs[] = line.toCharArray();
int mul = 1;
for (int i = chs.length - 1; i >= 0; i--) {
char c = chs[i];
switch (c) {
case '0':
break;
case '1':
total += mul;
break;
case '2':
total += (mul << 1);
break;
case '4':
total += (mul << 2);
break;
case '8':
total += (mul << 3);
break;
default:
total += (mul*((byte) c - (byte) ('0')));
}
mul*=10;
}
}
br.close();
return total;
}
运行时间为30.8秒!这比以前最好的速度提高了3倍
后续问题
字符串的开销吗?还有所有关于角色设置之类的幕后担忧
MappedByteBuffer
来提供帮助,我们能做得更好吗?我有一种感觉,调用方法从缓冲区读取的开销会减慢速度,特别是从缓冲区向后读取时String
的读取效率低下的原因与其说是创建所有String
对象所花费的时间太长,不如说是因为它们的寿命太短:我们有100000000个对象需要垃圾收集器处理。那肯定会使它心烦意乱
现在,一些实验基于人们发布的答案/评论
我在缓冲区的大小上作弊吗?
一个建议是,由于BufferedReader
使用了16KB的默认缓冲区,而我使用了8MB的缓冲区,所以我不会比较like和like。如果使用更大的缓冲区,速度肯定会更快
令人震惊的是。sumBinary()
方法(方法4)昨天使用8MB缓冲区在30.8秒内运行。今天,代码没变,风向变了,我们现在是30.4秒。如果我将缓冲区大小降低到16KB,看看它会变慢多少,它会变快现在运行时间为23.7秒。疯子谁看见那个人来了
一些实验表明16KB是最佳的。也许Java的人做了同样的实验,这就是为什么他们使用16KB
问题是I/O绑定的吗?
我也在想这件事。在磁盘访问上花费了多少时间,在数字运算上花费了多少时间?如果它几乎是所有的磁盘访问,正如对其中一个建议答案的一个支持良好的评论所建议的那样,那么无论我们做什么,我们都无法做出多大的改进
通过在注释掉所有解析和数字运算的情况下运行代码,这很容易进行测试,但读取内容仍然完整:
private long sumBinary() throws IOException {
RandomAccessFile raf = new RandomAccessFile(file, "r");
int lastRead = (int) raf.length();
byte buf[] = new byte[16 * 1024];
int mul = 1;
long total = 0;
while (lastRead > 0) {
int len = Math.min(buf.length, lastRead);
raf.seek(lastRead - len);
raf.readFully(buf, 0, len);
lastRead -= len;
/*for (int i = len - 1; i >= 0; i--) {
if ((buf[i] >= 48) && (buf[i] <= 57)) {
total += mul * (buf[i] - 48);
mul *= 10;
} else
mul = 1;
}*/
}
raf.close();
return total;
}
这将在20.0秒的时间内运行,比向后扫描版本快一段距离。很好
乘法缓存
然而,我在晚上意识到,尽管我在每次迭代中执行两次乘法,但有可能使用缓存来存储这些乘法,这样我就可以避免在反向迭代中执行乘法。我很高兴地看到,当我醒来时,有人有同样的想法
关键是,我们扫描的数字最多有10个数字,而且只有10个可能的数字,因此一个数字的值与累计总数的比值只有100个可能。我们可以预先计算这些,然后在反向扫描代码中使用它们。这应该比前向扫描版本要好,因为我们现在已经完全消除了乘法。(请注意,我们不能用正向扫描来实现这一点,因为乘法是累加器的乘法,累加器可以取10^9以内的任何值。只有在反向情况下,两个操作数都被限制在少数可能性内。)
这似乎确实改善了一些情况:我们现在的时间是19.0秒。我们已经采取了行动
private long sumBinary() throws IOException {
RandomAccessFile raf = new RandomAccessFile(file, "r");
int lastRead = (int) raf.length();
byte buf[] = new byte[16 * 1024];
int mul = 1;
long total = 0;
while (lastRead > 0) {
int len = Math.min(buf.length, lastRead);
raf.seek(lastRead - len);
raf.readFully(buf, 0, len);
lastRead -= len;
/*for (int i = len - 1; i >= 0; i--) {
if ((buf[i] >= 48) && (buf[i] <= 57)) {
total += mul * (buf[i] - 48);
mul *= 10;
} else
mul = 1;
}*/
}
raf.close();
return total;
}
private long sumBinaryForward() throws IOException {
RandomAccessFile raf = new RandomAccessFile(file, "r");
int fileLength = (int) raf.length();
byte buf[] = new byte[16 * 1024];
int acc = 0;
long total = 0;
int read = 0;
while (read < fileLength) {
int len = Math.min(buf.length, fileLength - read);
raf.readFully(buf, 0, len);
read += len;
for (int i = 0; i < len; i++) {
if ((buf[i] >= 48) && (buf[i] <= 57))
acc = acc * 10 + buf[i] - 48;
else {
total += acc;
acc = 0;
}
}
}
raf.close();
return total;
}
private long sumBinaryCached() throws IOException {
int mulCache[][] = new int[10][10];
int coeff = 1;
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++)
mulCache[i][j] = coeff * j;
coeff *= 10;
}
RandomAccessFile raf = new RandomAccessFile(file, "r");
int lastRead = (int) raf.length();
byte buf[] = new byte[16 * 1024];
int mul = 0;
long total = 0;
while (lastRead > 0) {
int len = Math.min(buf.length, lastRead);
raf.seek(lastRead - len);
raf.readFully(buf, 0, len);
lastRead -= len;
for (int i = len - 1; i >= 0; i--) {
if ((buf[i] >= 48) && (buf[i] <= 57))
total += mulCache[mul++][buf[i] - 48];
else
mul = 0;
}
}
raf.close();
return total;
}
private long sumBinaryForwardMap() throws IOException {
RandomAccessFile raf = new RandomAccessFile(file, "r");
byte buf[] = new byte[16 * 1024];
final FileChannel ch = raf.getChannel();
int fileLength = (int) ch.size();
final MappedByteBuffer mb = ch.map(FileChannel.MapMode.READ_ONLY, 0,
fileLength);
int acc = 0;
long total = 0;
while (mb.hasRemaining()) {
int len = Math.min(mb.remaining(), buf.length);
mb.get(buf, 0, len);
for (int i = 0; i < len; i++)
if ((buf[i] >= 48) && (buf[i] <= 57))
acc = acc * 10 + buf[i] - 48;
else {
total += acc;
acc = 0;
}
}
ch.close();
raf.close();
return total;
}
private class SumTaskResult {
long subtotal;
int leftPartial;
int leftMulCount;
int rightPartial;
public void append(SumTaskResult rightward) {
subtotal += rightward.subtotal + rightPartial
* rightward.leftMulCount + rightward.leftPartial;
rightPartial = rightward.rightPartial;
}
}
private class SumForkTask extends RecursiveTask<SumTaskResult> {
private byte buf[];
// startPos inclusive, endPos exclusive
private int startPos;
private int endPos;
public SumForkTask(byte buf[], int startPos, int endPos) {
this.buf = buf;
this.startPos = startPos;
this.endPos = endPos;
}
private SumTaskResult computeDirectly() {
SumTaskResult result = new SumTaskResult();
int pos = startPos;
result.leftMulCount = 1;
while ((buf[pos] >= 48) && (buf[pos] <= 57)) {
result.leftPartial = result.leftPartial * 10 + buf[pos] - 48;
result.leftMulCount *= 10;
pos++;
}
int acc = 0;
for (int i = pos; i < endPos; i++)
if ((buf[i] >= 48) && (buf[i] <= 57))
acc = acc * 10 + buf[i] - 48;
else {
result.subtotal += acc;
acc = 0;
}
result.rightPartial = acc;
return result;
}
@Override
protected SumTaskResult compute() {
if (endPos - startPos < 64)
return computeDirectly();
int mid = (endPos + startPos) / 2;
SumForkTask left = new SumForkTask(buf, startPos, mid);
left.fork();
SumForkTask right = new SumForkTask(buf, mid, endPos);
SumTaskResult rRes = right.compute();
SumTaskResult lRes = left.join();
lRes.append(rRes);
return lRes;
}
}
private long sumBinaryForwardMapForked() throws IOException {
RandomAccessFile raf = new RandomAccessFile(file, "r");
ForkJoinPool pool = new ForkJoinPool();
byte buf[] = new byte[1 * 1024 * 1024];
final FileChannel ch = raf.getChannel();
int fileLength = (int) ch.size();
final MappedByteBuffer mb = ch.map(FileChannel.MapMode.READ_ONLY, 0,
fileLength);
SumTaskResult result = new SumTaskResult();
while (mb.hasRemaining()) {
int len = Math.min(mb.remaining(), buf.length);
mb.get(buf, 0, len);
SumForkTask task = new SumForkTask(buf, 0, len);
result.append(pool.invoke(task));
}
ch.close();
raf.close();
pool.shutdown();
return result.subtotal;
}
public static void genRandoms() {
Random r = new Random();
for (int i = 0; i < 100000000; i++)
System.out.println(r.nextInt(1000000000));
}
BufferedReader br = new BufferedReader(new InputStreamReader(
new FileInputStream(file), StandardCharsets.US_ASCII),
1_024_000_000);
// 4k buffer size.
static final int SIZE = 4 * 1024;
static byte[] buffer = new byte[SIZE];
// Fastest because a FileInputStream has an associated channel.
private static void ScanDataFile(Hunter p, FileInputStream f) throws FileNotFoundException, IOException {
// Use a mapped and buffered stream for best speed.
// See: http://nadeausoftware.com/articles/2008/02/java_tip_how_read_files_quickly
final FileChannel ch = f.getChannel();
long red = 0L;
do {
final long read = Math.min(Integer.MAX_VALUE, ch.size() - red);
final MappedByteBuffer mb = ch.map(FileChannel.MapMode.READ_ONLY, red, read);
int nGet;
while (mb.hasRemaining() && p.ok()) {
nGet = Math.min(mb.remaining(), SIZE);
mb.get(buffer, 0, nGet);
for (int i = 0; i < nGet && p.ok(); i++) {
p.check(buffer[i]);
//size += 1;
}
}
red += read;
} while (red < ch.size() && p.ok());
// Finish off.
p.close();
ch.close();
f.close();
}
class Summer {
long sum = 0;
long val = 0;
public void add(byte b) {
if (b >= '0' && b <= '9') {
val = (val * 10) + (b - '0');
} else {
sum += val;
val = 0;
}
}
public long getSum() {
return sum + val;
}
}
private long sumMapped() throws IOException {
Summer sum = new Summer();
FileInputStream f = new FileInputStream(file);
final FileChannel ch = f.getChannel();
long red = 0L;
do {
final long read = Math.min(Integer.MAX_VALUE, ch.size() - red);
final MappedByteBuffer mb = ch.map(FileChannel.MapMode.READ_ONLY, red, read);
int nGet;
while (mb.hasRemaining()) {
nGet = Math.min(mb.remaining(), SIZE);
mb.get(buffer, 0, nGet);
for (int i = 0; i < nGet; i++) {
sum.add(buffer[i]);
}
}
red += read;
} while (red < ch.size());
// Finish off.
ch.close();
f.close();
return sum.getSum();
}
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file), 8*1024*1024/2);
long total = 0;
int i;
while ((i = bis.read()) != -1)
{
byte b = (byte)i;
long number = 0;
while (b >= '0' && b <= '9')
{
number = number*10+b-'0';
if ((i = bis.read()) == -1)
break;
b = (byte)i;
}
total += number;
}