-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Reduce stat calls for wildcard #10032
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
This makes it so expand_intermediate_segment knows about the case where it's last, only followed by a "/". When it is, it can do without the file_id for finding links (we don't resolve the files we get here), which allows us to remove a stat() call. This speeds up the case of `*/` or `.../...*.../` by quite a bit. If that last component was a directory with 1000 subdirectories we could skip 1000 stat calls!
e47888b to
b5b449b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This causes a visible change when the final path component is a symlink loop. You can reproduce like so, in an initially empty directory:
> ln -s . loop
> echo */*/
Before this change, the wildcard matching would fail, because we would notice that the directory had already been visited. After this change, we no longer check for a visited directory in the last intermediate segment (the second *) so it outputs loop/loop/.
I'm not sure if this behavior is better or worse: symlink loops are mainly a problem for recursive wildcards. But we should probably decide which behavior we want before making a behavior change.
Meanwhile, I think you can still get almost all the perf benefit by only using the fast path if the directory entry is not a symlink, which is returned from the d_type. This would require adding that information to dir_iter_t::entry_t.
If I understand this correctly, loop/loop would be printed once, it would not be an infinite recursion? I would find that acceptable in a vacuum, but I'm wary of the change.
That should work, because we do enter the link once - if there's a directory with "foo" and "loop", we get "foo" and "loop/foo". The one we don't get is "loop/loop". So we really just need the information that we've seen this link already. |
This skips a stat() in a common case, making `*/` wildcards a bunch faster.
|
If I do add the link fast path (add a "possible_link" bool that is set to false if we don't get DT_LNK or DT_UNKNOWN), it fails this check: # But symlink loops only get explored once.
mkdir -p dir1/child2/grandchild1
touch dir1/child2/grandchild1/differentfile
ln -s ../../child2/grandchild1 dir1/child2/grandchild1/link2
echo **/differentfile
# CHECK: dir1/child2/grandchild1/differentfileBecause that expects the path via the link to not be counted because it knows we got the file. This would require us to notice we already visited "differentfile". And because the first path to that does not go via any link, and the link starts at "dir1/", we would need to stat() dir1/ without it being a link. Honestly I am not entirely sure I understand the behavior here. I get that we want to stop symlink loops. But it only really stops for the It is fundamentally weird to me that Anyway, given the above I do not believe this is possible without a behavior change. |
|
Okay, this now only skips the file_id_t if it isn't a link and it's the last element. That means It's unfortunately a pretty narrow case, and I still don't understand why the loop detection is that way. But for that narrow and reasonably common case it's a sweet and reasonably simple fix. |
ridiculousfish
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice fix! I agree the current behavior is a little confusing but this is a great optimization in the mean time. One thing to fix.
src/wutil.cpp
Outdated
| entry_.type_ = type; | ||
| // But store if we know it can't be a link. | ||
| // If it is unknown, it could still be a link. | ||
| entry_.possible_link_ = !type.has_value(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
entry_.possible_link_ should be set to true or false in every path in this function, as we re-use the entry_ variable to save allocations.
This makes it so expand_intermediate_segment knows about the case where it's last, only followed by a "/". When it is, it can do without the file_id for finding links (we don't resolve the files we get here), which allows us to remove a stat() call. This speeds up the case of `...*/` by quite a bit. If that last component was a directory with 1000 subdirectories we could skip 1000 stat calls! One slight weirdness: We refuse to add links to directories that we already visited, even if they are the last component and we don't actually follow them. That means we can't do the fast path here either, but we do know if something is a link (if we get d_type), so it still works in common cases. (cherry picked from commit 86803e4)
Description
This attempts to reduce the number of stat calls we do for wildcard in the case of a final
*/component. That*is taken as an "intermediate" segment, followed by an empty final segment. This makes it so the intermediate segment expansion knows about it and stops the stat() calls we do for the purpose of symlink cycle detection (since we do not enter any symlinks we can't add any new cycles there!).true ~/complete_test/*/with acomplete_testdirectory with 10000 subdirectories is sped up by 1.60x relative to 35baa88 by the first commit.This is on ext4 with proper d_type support, so we get all the info via readdir, and can save 10k stat calls for 10k files
This is in C++ because that's the version that's currently used. If we decide it's a good approach, I'll port it to rust, and we can easily cherry-pick it for 3.7.0.
(I also had something for
ls ~/foo/<TAB>, but that didn't end up saving anything and was kinda messy so I removed it - ignore the second commit)TODOs: