Skip to content

Conversation

aG0aep6G
Copy link
Contributor

Inspired by Andrei's call for "scaling up rdmd" (forum post, issue 14654).

But this doesn't implement Andrei's idea. Instead it does what Jacob Carlborg and myself thought might be a better approach: Compile all outdated files, and only those, with one compiler invocation to multiple object files. Seems to work fine.

See commit message for a summary of the process.

I'm planning to add some tests to rdmd_test tomorrow. But code-wise I'm happy with it now; ready to get destroyed.

@dnadlinger
Copy link
Contributor

This won't work in the general case. DMD still emits template instances to the first module specified on the command line if it can't figure out where they should really go. Right now, incremental compilation is only guaranteed to work if you pass the exact same list of modules every time.

@CyberShadow
Copy link
Member

DMD still emits template instances to the first module specified on the command line if it can't figure out where they should really go.

Is this still true? Is there a self-contained test case for this?

@aG0aep6G
Copy link
Contributor Author

This won't work in the general case. DMD still emits template instances to the first module specified on the command line if it can't figure out where they should really go. Right now, incremental compilation is only guaranteed to work if you pass the exact same list of modules every time.

Bah. Do you have an example of when dmd can't figure it out?

string workDir, string objDir, in string[string] myDeps,
string[] compilerFlags, bool addStubMain)
private int link(in string fullExe, in string[] objects,
in string[] compilerFlags, in string workDir)
{
version (Windows)
fullExe = fullExe.defaultExtension(".exe");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't compile on Windows any more because fullExe is an in parameter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@jacob-carlborg
Copy link

This won't work in the general case. DMD still emits template instances to the first module specified on the command line if it can't figure out where they should really go. Right now, incremental compilation is only guaranteed to work if you pass the exact same list of modules every time.

@klickverbot "Whatever the matters are, we will fix them" [1].

[1] http://forum.dlang.org/post/mkv452$1tdo$4@digitalmars.com

@jacob-carlborg
Copy link

What about using a hash based on the files instead of using a timestamp?

@aG0aep6G
Copy link
Contributor Author

What about using a hash based on the files instead of using a timestamp?

This hadn't crossed my mind. Spontaneous thoughts about it:

  • It would be more expensive. Maybe significantly so.
  • When hashes collide things go wrong in a way that's hard to understand for the user. Probably not a problem in reality.
  • It should be possible to retrofit that later on.
  • What problem do hashes solve? When do timestamps fail?

@aG0aep6G
Copy link
Contributor Author

Added some tests. I'm not super happy with Thread.sleeping to get different timestamps, but I don't see another sane way. Setting modification times manually would require touching the object files.

@jacob-carlborg
Copy link

What problem do hashes solve? When do timestamps fail?

The file is only recompiled if the content has changed, even if the timestamp has been updated.

aG0aep6G added 2 commits June 15, 2015 23:16
Summary of the process:
Build a dependency graph (getDependencies).
For every source file, figure out when the last change happened to the
file itself or any of its dependencies (getTimes).
Split source files up into two groups:
 * Put files that need to be (re-)compiled into toCompile.
 * Put files for which an older object file can be reused into toReuse.
Compile all files in toCompile in one compiler call (compile).
Link all new and old objects together in another compiler call (link).
Also move the temporary directory for the tests to /tmp/rdmd_test. We
generate quite some files and we don't want to overwrite the stuff of
others.
@DmitryOlshansky
Copy link
Member

When hashes collide things go wrong in a way that's hard to understand for the user. Probably not a problem in reality.

I suggest you actually try to find a collision even in (technically "broken") MD5. It's not something happening except w/o considerable effort, compute power and time spent to finding collision. Note that git we are using is build on the assumption that SHA1 hash won't have a collision in one repo. In fact most often people use first 8 numbers of hash to checkout some commit.

It would be more expensive. Maybe significantly so.

I believe computing checksum of sources should be fairly cheap, sources rarely exceed a few Mb in total. Phobos is about 9Mb with all the fluf and generated tables in std/internal ticking in at 1.6Mb. Speed depends on CPU but is in the range 200+Mb/s on circa 2011 hardware.

http://stackoverflow.com/questions/2722943/is-calculating-an-md5-hash-less-cpu-intensive-than-sha-family-functions

@aG0aep6G
Copy link
Contributor Author

Heads up: I think I've fucked things up with getTimes. I'm going to look at that again and will likely change things.

@CyberShadow
Copy link
Member

I believe computing checksum of sources should be fairly cheap

The biggest bottleneck is going to be reading all source code from disk for every rdmd run (even if nothing changed). You could use timestamps AND checksums, but it's still an unnecessary performance hit.

Very few build systems make the decision to use checksums because it is usually simply unnecessary. It only makes sense in situations with non-trivial caching systems (where a --force switch is impractical or isn't supposed to need to exist).

@aG0aep6G
Copy link
Contributor Author

Heads up: I think I've fucked things up with getTimes. I'm going to look at that again and will likely change things.

Update: I now think everything's fine. I may have been staring at this for too long. Getting the newest timestamp of a file and its dependencies, and then comparing that to the object file - that's sound, isn't it?

// Validate extensions of extra files (--extra-file)
foreach (immutable f; extraFiles)
{
if (![".d", ".di", objExt].canFind(f.extension))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (!only(".d", ".di", objExt).canFind(f.extension))

}
}

immutable rootDir = dirName(rootModule);
return deps;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At 220 lines, this seems to be quite a long function. I think it should be broken into smaller ones.

@andralex
Copy link
Member

This is a bit heavier than what I'd hope for what it does, but I found no obvious bilgewater. It does raise an eyebrow that a 1KLOC program needs a HashSet on top of an associative array instead of just making use of the respective associative array.

Regarding discussion of using hashes instead of timestamps - that's a fine idea (git and others do that), but it's outside the charter of this PR, and may be implemented independently of it. Please focus reviews on the design and implementation of this PR.

@aG0aep6G: did you run speed measurements?

@aG0aep6G
Copy link
Contributor Author

Followed Andrei's suggestions or replied when I didn't.

@aG0aep6G
Copy link
Contributor Author

@aG0aep6G: did you run speed measurements?

Not extensively. I did time a project of mine (about 8 kloc).
Nothing changed for --force builds.
A build without any changes went up from 0.034s to 0m0.073s
Building when just the main file changed takes about as long as a full rebuild. But the issued commands are fine. The program's just template heavy.

A toy example where an imported file takes long to compile works nicely:

module main;
import slow_compile;
void main() {}

module slow_compile;
struct S(ulong depth)
{
    static if(depth > 0)
    {
        S!(depth - 1) sub1;
        S!(depth - 1) sub2;
    }
}
alias Instance = S!15;

3s for a full build, 0.2s when only main was touched.

@rainers
Copy link
Member

rainers commented Jun 16, 2015

This won't work in the general case. DMD still emits template instances to the first module specified on the command line if it can't figure out where they should really go.

Here is an example of what David referred to:

module main;
import imp;
import foo;

Struc!int x; // remove this line for second build

void main()
{
    fun();
}
module foo;
import imp;

int fun()
{
    Struc!int ix;
    return ix.get();
}
module imp;

struct Struc(T)
{
    T x;
    T get() { return x; }
}

Module imp.d is meant as an import from a library like std.algorithm, so not compiled with the other files. Making an example where it's compiled aswell is a bit harder, but not really.
Compile with dmd -c main.d foo.d, then link with dmd main.obj foo.obj. Everything fine.

Then remove the commented line in main.d, recompile with dmd -c main.d and link:

OPTLINK (R) for Win32  Release 8.00.17
Copyright (C) Digital Mars 1989-2013  All rights reserved.
http://www.digitalmars.com/ctg/optlink.html
foo.obj(foo)
 Error 42: Symbol Undefined _D3imp12__T5StrucTiZ5Struc3getMFNaNbNiNfZi

In the first run, Struc!int is built into main.obj, so it's gone after being removed from main.d.

I think an easier approach to incremental compilation is when dmd supports replacing existing symbols in a library. It accumulates all used template instances ever, though, so it might grow indefinitely...

@DmitryOlshansky
Copy link
Member

The biggest bottleneck is going to be reading all source code from disk for every rdmd run (even if nothing changed).

That's true.

@WalterBright
Copy link
Member

DMD still emits template instances to the first module specified on the command line if it can't figure out where they should really go.

No. DMD, when presented with multiple modules on a command line builds exactly ONE combined object file, not one per module. Templates are instantiated and inserted into that object file which the compiler does not see instantiated by one of the imported modules.

Oh, I see what you're doing, you're using -c. That does present a problem with "where does the template instance go". No obvious solution, other than using -allinst.

@andralex
Copy link
Member

So... we're kind of stuck here. @WalterBright what's the way out?

@rainers
Copy link
Member

rainers commented Jun 20, 2015

Oh, I see what you're doing, you're using -c. That does present a problem with "where does the template instance go". No obvious solution, other than using -allinst.

The problem is that it is generated into only one of the object files and might disappear there with source code changes while another object file still depends on it. -allinst won't help.

@jacob-carlborg
Copy link

Would there be a problem if the compiler outputted the symbols in all object files? Doesn't LDC do that?

@dnadlinger
Copy link
Contributor

@jacob-carlborg: LDC (mostly) uses the same symbol emission strategy as DMD.

@aG0aep6G
Copy link
Contributor Author

Would there be a problem if the compiler outputted the symbols in all object files?

Basically what happens with separate compilation, no?

Sticking with Rainer's example, this works:

dmd -c main.d
dmd -c foo.d
dmd main.o foo.o
# remove the marked line from main.d
dmd -c main.d
dmd main.o foo.o # no problem

nm shows that there's weak duplicates in the object files (before removing the marked line). nm main.o and nm foo.o both include:

0000000000000000 W _D3imp12__T5StrucTiZ5Struc3getMFNaNbNiNfZi
0000000000000000 V _D3imp12__T5StrucTiZ5Struc6__initZ
0000000000000000 W _D3imp15__unittest_failFiZv
0000000000000000 W _D3imp7__arrayZ
0000000000000000 W _D3imp8__assertFiZv

@dnadlinger
Copy link
Contributor

Would there be a problem if the compiler outputted the symbols in all object files?

Basically what happens with separate compilation, no?

Yes. At the risk of sounding like a broken record, incremental compilation right now is only guaranteed to work if any given module is only compiled as part of one list of source files, which cannot be changed as long as you want to reuse other object files. Only ever compiling a single module at once trivially satisfies this criterion.

@MartinNowak
Copy link
Member

Compiling multiple object files in one go is fundamentally broken and we're unlikely to fix that b/c we decided to emit as few template instances as possible rather than to always emit all into every object file.
You should follow Andreis' original proposal and rebuild static libraries for each package or use dub which already knows how to do this efficiently.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants