New issue
Advanced search Search tips
Note: Color blocks (like or ) mean that a user may not be available. Tooltip shows the reason.

Issue 742525 link

Starred by 2 users

Issue metadata

Status: Archived
Owner:
Closed: Feb 2018
Cc:
EstimatedDays: ----
NextAction: ----
OS: Android
Pri: 3
Type: Bug

Blocked on:
issue 742655



Sign in to add a comment

Unused string literals retained in binaries (fixed by switching to lld)

Project Member Reported by torne@chromium.org, Jul 13 2017

Issue description

String literals that are used in their translation unit, but only by code which is deleted by -Wl,--gc-sections, are retained in the binaries on Android (and probably all platforms where we use clang?), because the strings are all just put into .rodata and so can't be collected.

gcc implemented a solution to this in https://gcc.gnu.org/bugzilla/show_bug.cgi?id=192 - when -fmerge-constants and -fdata-sections are used together, each string literal is put in a separate section to allow cross-translation unit merging and collection of unused literals at --gc-sections time. It looks like -fmerge-constants is a no-op in clang, however, and this doesn't happen there.

I'm also surprised that despite *not* being able to remove unused string literals at --gc-sections time, it *does* somehow collapse identical string literals together from different translation units during linking, even though the input objects have all their strings merged together in one section. I guess I don't get how linkers work? ;)
 

Comment 1 by torne@chromium.org, Jul 13 2017

This would save a minimum of 80kb on the current android monochrome binary (the JNI registration info that's never used in monochrome), and will probably save some amount for all binaries since I'm sure there are other unreferenced string literals.

Comment 2 by thakis@chromium.org, Jul 13 2017

Cc: ruiu@google.com p...@chromium.org r...@chromium.org h...@chromium.org
Labels: clang
You don't happen to have some repro smaller than "build chrome"?

How would we verify that we fixed this?

I.e. "concrete repro steps, please" :-)

80kB savings sounds pretty good though!

Comment 3 by p...@chromium.org, Jul 13 2017

I don't know what gold does, but lld's implementation of --gc-sections should normally remove unused string literals, as long as they are in a section marked with the SHF_MERGE flag. clang does this unconditionally and gcc does it at -O1 or higher (or, as you mention, if you pass -fmerge-constants).

Comment 4 by ruiu@google.com, Jul 13 2017

Strings in sections with SHF_MERGE and SHF_STRINGS bits are merged by the linker. I think the thing you observed is that feature.

Comment 5 by torne@chromium.org, Jul 13 2017

Oh sorry, I made a pretty small repro and then forgot to attach it :)

I compiled this with:
g++ -O2 -ffunction-sections -fdata-sections -Wl,--gc-sections
(and the same for clang++)
and then dumped the .rodata section in the output. gcc ends up with .rodata just containing "hi there\0" but clang retains all the strings from kMethodsFoo as well.

typedef struct {
  const char* name;
  const char* signature;
  void* fnPtr;
} JNINativeMethod;

static const JNINativeMethod kMethodsFoo[] = {
  { "nativeInit",
    "()J",
    0 },
  { "nativeLoadsOfStuff",
    "(Lorg/chromium/foo/Bar;II)V",
    0 },
};

extern bool RegisterNatives(const JNINativeMethod*);

bool RegisterNative_Foo() {
  return RegisterNatives(kMethodsFoo);
}

int main(int argc, const char** argv) {
  argv[0] = "hi there";
  return 0;
}

Comment 6 by torne@chromium.org, Jul 13 2017

Is lld the default in the clang toolchain in chromium? I didn't explicitly specify the linker.

Comment 7 by thakis@chromium.org, Jul 13 2017

No, we currently still use gold almost everywhere.

Comment 8 by p...@chromium.org, Jul 13 2017

But we currently link official builds on Linux with lld.

I can confirm that I can reproduce this issue with bfd and gold, but not with lld. So one way to fix this might be to switch to lld on Android (for official builds, at least).

$ cat foo.sh
#!/bin/bash

for c in g++ clang++; do
  for  l in bfd gold lld; do
    $c -O2 -ffunction-sections -fdata-sections torne.cc -c
    ~/src2/llvm-project7/ra/bin/clang++ -fuse-ld=$l -Wl,--gc-sections torne.o
    
    echo "$c $l"
    strings -a a.out | grep nativeL
  done
done

$ sh foo.sh 
g++ bfd
nativeLoadsOfStuff
g++ gold
nativeLoadsOfStuff
g++ lld
clang++ bfd
nativeLoadsOfStuff
clang++ gold
nativeLoadsOfStuff
clang++ lld

Comment 9 by p...@chromium.org, Jul 13 2017

(lld on ARM is still not quite there yet though IIUC.)

Comment 10 by p...@chromium.org, Jul 14 2017

Filed  issue 742655  to track switching to lld on Android.

Comment 11 by torne@chromium.org, Jul 14 2017

Aha; thanks for the explanation, I get how SHF_MERGE and SHF_STRINGS is able to achieve the deduplication, and it also makes sense that lld can remove unreferenced strings this way. So now I'm just disappointed that bfd/gold can't do the same thing and require gcc to emit everything into sections instead ;)

It does sound like solving the blockers and switching to lld is the right way to deal with this.
Did some related exploration in  bug 746761 . Looks like if we change jni generator to declare strings as:

const char[] kStr1 = "...";

Then the strings will get proper symbol names, and also gc'ed when linked. The (killer) caveat is that this also causes them to not be merged with identical strings.

Nothing actionable here, but I found it interesting that there was a difference between [] and const *, so thought I'd share :).
You can also add `const char* const kStr = ...`, then that should happen too. Without the second const, the pointer itself could be changed.

I didn't know that this had an influence on icf.
Have a look at  bug 746761 , having that second const doesn't help (although maybe should?).

I'm guessing maybe it's just the difference between "this is a string literal" and, "this is an array of characters". Perhaps C enforces that arrays have unique addresses?

We have 15000 const char[] as of now:
https://cs.chromium.org/search/?q=const%5C+char%5C+%5B%5E*%5D*%5C%5B%5C%5D%5C+%3D%5C+%5C%22&sq=package:chromium&type=cs

This shows that "hostname" occurs twice (admittedly not conclusive)
strings libmonochrome.so| grep hostname 

flags show up 4 times:
strings libmonochrome.so| grep '^flags$'
Summary: Unused string literals retained in binaries (fixed by switching to lld) (was: Unused string literals retained in binaries)
Status: Assigned (was: Untriaged)
Blockedon: 742655
Cc: pasko@chromium.org
Status: Archived (was: Assigned)
We've now switched to lld. The original JNI motivation has already been solved separately. Closing as no work to be done here.
Did we actually get a binsize win from switching to lld?

Comment 22 by p...@chromium.org, Feb 7 2018

Yes, the monochrome APK size decreased by 1.6MB.

https://chromeperf.appspot.com/group_report?rev=531064

Comment 23 by ruiu@google.com, Feb 7 2018

I wonder if you guys have enabled string tail merging. Please make sure that you passed -O2 to lld to enable the feature.

Comment 24 by p...@chromium.org, Feb 7 2018

We're currently passing -O1. Looks like -O2 is a 150KB win in libmonochrome.so, let's go for it.

Comment 25 by p...@chromium.org, Feb 7 2018

Filed issue 810154.

Comment 26 by ruiu@google.com, Feb 7 2018

When I build Android Chrome on my machine, it looks like generate .so files contain not .gnu.hash but .hash sections. Is this intentional? Using .gnu.hash may shrink the file size even more.
Which option controls which style of hash is emitted?

Comment 28 by ruiu@google.com, Feb 8 2018

--hash-style={sysv,gnu,both}. Default is both so that a generated binary file runs on old operating systems. GNU hash is a new and better one, so unless we are creating DSOs for very old system, we should use --hash-style=gnu.
Why do operating system launchers care about the executable hash?

Comment 30 by ruiu@google.com, Feb 8 2018

Resolving a dynamic symbol is expensive without these sections. We have a list of DSOs from which dynamic symbols should be resolved, and for each dynamic symbol, we look up the DSOs's symbol table one by one until we find a match. So it is O(n*m) where n and m are the number of DSOs and the number of dynamic symbols. For large programs, both n and m are large.

To reduce the cost of lookup, DSOs have on-disk hash tables (and bloom filters if .gnu.hash). That saves a lot of time because hash table lookup is fast.
Oh, it's a hash table, not a hash. I had confused this with the -Wl,--build-id hash. Thanks for clarifying!

So we should use --hash-style=gnu.

Maybe that should be the default if it works on all but "very old" OSs? If someone needs to build for such an old OS, they're likely using an older linker, and if they aren't they could use the flag to ask for the old behavior. Maybe the flag default should be good for the common case.
https://chromium-review.googlesource.com/#/c/chromium/src/+/907449 , but changing the default since most programs on this world won't pass this flag explicitly.

Comment 33 by ruiu@google.com, Feb 8 2018

Users don't need to worry about it in most cases because (I believe) the compiler automatically passes --hash-style=both to the linker. Unless you want to save space by eliminating .hash, that default flag should work fine. So, maybe we should change the linker's default to --hash-style=gnu to modernize it, it is perhaps not that important.
Yes, I meant we should change the compiler driver, so that =gnu becomes the default for saving space if it's no longer needed in practice almost all of the time.

But now that I look at the driver source:
http://llvm-cs.pcc.me.uk/tools/clang/lib/Driver/ToolChains/Linux.cpp#237

  // Android loader does not support .gnu.hash.

O_o

https://android-review.googlesource.com/c/platform/build/soong/+/456718/2/cc/linker.go says it works on "newer devices"; I'll ask around to find out what that means.

Comment 35 by ruiu@google.com, Feb 8 2018

Sigh. lld's sysv-style .hash section is very unoptimized because we focus on producing good .gnu.hash option and maintain .hash section just for backward compatibility. We should improve lld so that it produces better .hash section.
Android supports .gnu.hash since M. So we should use --hash-style=both for Android for the time being. Sounds like we currently only emit the sysv hash on Android, so =both might help startup a bit.

Comment 37 by p...@chromium.org, Feb 8 2018

The monochrome APK requires N, so we could potentially use gnu in libmonochrome and both in libchrome.
Enabling for libmonochrome is quite convenient, as libchrome hides all JNI symbols, leaving *very* few exported symbols. libmonochrome.so on the other hand exports all JNI symbols, so has significantly more.
I have a build around..

This many symbols is exported:

shell> objdump=third_party/android_tools/ndk/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-objdump

shell> $objdump -j .text -T gn_android/ReleaseOfficial/libmonochrome.so | grep '^[0-9a-f]' | wc -l
1917

shell> $objdump -j .text -T gn_android/ReleaseOfficial/libchrome.so | grep '^[0-9a-f]' | wc -l
1

oh, that's *very* few :)

libmonochrome hash sizes:

shell> $objdump -h gn_android/ReleaseOfficial/libmonochrome.so | grep hash | awk '{ sz = strtonum("0x"$3); printf ("%s - %s bytes\n", $2, sz); }'
.gnu.hash - 11648 bytes
.hash - 18280 bytes

Saving 11K with --hash-style=gnu seems worth it :)

Also found that all the symbols are auto-generated JNI names, 160KiB in total, urges my inner geek to compress them, but then the inconvenience of maintaining our own demangler for 150KiB of binary size .. probably is not worth it
pardon, it would be saving 18KiB
Yeah, it should be fine to switch hash style for monochrome.

Trying to compress those symbols would defeat the point of them being there - that the VM knows how to look them up on demand, avoiding a startup time penalty to register them all manually. Also, the manual registration code used in other builds I believe is of a comparable size - it has to contain all the Java type information, class names, and function names in the JNI symbol names anyway as string literals to pass to the VM, so while there's a bit less string data due to not repeating the class names as much and the strings being slightly more likely to be duplicates that can be collapsed, the added code to actually do the registration probably makes up for it..
Project Member

Comment 42 by bugdroid1@chromium.org, Feb 9 2018

The following revision refers to this bug:
  https://chromium.googlesource.com/chromium/src.git/+/f0423b008b159db5e4967e87ff503140679d22d6

commit f0423b008b159db5e4967e87ff503140679d22d6
Author: Andrew Grieve <agrieve@chromium.org>
Date: Fri Feb 09 05:19:58 2018

Set --hash-style=gnu for libmonochrome.so

Shrinks libmonochrome.so by 16kb.

Bug:  742525 
Change-Id: I8c772feac1b09c2ebe68bb237229839050e541db
Reviewed-on: https://chromium-review.googlesource.com/909711
Reviewed-by: Nico Weber <thakis@chromium.org>
Commit-Queue: agrieve <agrieve@chromium.org>
Cr-Commit-Position: refs/heads/master@{#535648}
[modify] https://crrev.com/f0423b008b159db5e4967e87ff503140679d22d6/chrome/android/BUILD.gn

Project Member

Comment 43 by bugdroid1@chromium.org, Mar 20 2018

The following revision refers to this bug:
  https://chromium.googlesource.com/chromium/src.git/+/02d3c5b9207ffa9d4eb3ce7480bf2c21fec2902b

commit 02d3c5b9207ffa9d4eb3ce7480bf2c21fec2902b
Author: Peter Collingbourne <pcc@google.com>
Date: Tue Mar 20 14:46:18 2018

Also pass --hash-style=gnu when building the android_webview libmonochrome.so.

Shrinks the apk size by another 12kb.

Bug:  742525 
Change-Id: I97e889cca1adb52da69490c57094565121d0b52a
Reviewed-on: https://chromium-review.googlesource.com/970083
Commit-Queue: Richard Coles <torne@chromium.org>
Reviewed-by: agrieve <agrieve@chromium.org>
Reviewed-by: Richard Coles <torne@chromium.org>
Cr-Commit-Position: refs/heads/master@{#544361}
[modify] https://crrev.com/02d3c5b9207ffa9d4eb3ce7480bf2c21fec2902b/android_webview/BUILD.gn

Sign in to add a comment