-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Expose more string APIs #15075
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
base: master
Are you sure you want to change the base?
Expose more string APIs #15075
Conversation
Nice contribution! Thanks. About suggestions related to the naming scheme, I don't like
This is what other projects do:
|
I agree with @javiereguiluz about the word That said, I do not know the exact heuristics used in compilers to determine if some function is inlined, and they are likely complex. |
I would be in favor of that personally for the cases where I added One bothersome bit about that though is that |
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.
I'm happy with removal of the inline
qualifier in ext/spl and ext/session if you want to merge those changes already.
071dfd2
to
cf0168f
Compare
I've expanded the number of |
Could someone confirm that the symbols show up as expected on Windows? I don't know how to check this for that platform. If anyone knows if we check for existence of certain symbols in CI pipelines, I could add something there. |
To entertain the idea of #include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#define IS_STR_INTERNED (1 << 0)
#define IS_STR_PERSISTENT (1 << 1)
#define EXPECTED(condition) __builtin_expect(!!(condition), 1)
#define UNEXPECTED(condition) __builtin_expect(!!(condition), 0)
#define pefree(ptr, persistent) ((persistent)?free(ptr):efree(ptr))
extern void efree(void *);
typedef struct {
int32_t rc;
int32_t flags;
size_t len;
char val[1];
} zend_string;
extern inline void zend_string_release(zend_string *s) {
if (!(s->flags & IS_STR_INTERNED) && --s->rc == 0) {
pefree(s, s->flags & IS_STR_PERSISTENT);
}
}
void test1(zend_string *s) {
zend_string_release(s);
}
__attribute__((cold)) void test2(zend_string *s) {
zend_string_release(s);
}
void test3(zend_string *s, bool cond) {
if (UNEXPECTED(cond)) {
zend_string_release(s);
}
}
void test4(zend_string *s, bool cond) {
if (EXPECTED(cond)) {
zend_string_release(s);
}
}
|
I would have hoped for this matrix:
Because it's consistent with expected/unexpected code paths. It's interesting that GCC and Clang aren't consistent with each other. But this further solidifies the value in having both an inlined func and one that isn't, at least for some cases. |
Agreed.
It might be worth discussing with the GCC/Clang folks first, before committing to a big amount of code churn. If GCC doesn't want to adjust it behavior, I won't object to this change. The Clang behavior is less problematic. Inlining within cold functions is less problematic because they likely live in .cold sections. However, unlikely branches only get moved to .cold sections after some size threshold, so they can increase instruction cache pressure. |
To entertain the diff --git a/Zend/zend_string.c b/Zend/zend_string.c
index 7a00171d2c..d753582614 100644
--- a/Zend/zend_string.c
+++ b/Zend/zend_string.c
@@ -528,3 +528,7 @@ size_t strlcat (char *__restrict dest, const char *restrict src, size_t n)
return result;
}
#endif
+
+extern inline void zend_string_release(zend_string *s);
+extern inline uint32_t zval_gc_flags(uint32_t gc_type_info);
+extern inline uint32_t zend_gc_delref(zend_refcounted_h *p);
diff --git a/Zend/zend_string.h b/Zend/zend_string.h
index b62875a6ec..26a834692a 100644
--- a/Zend/zend_string.h
+++ b/Zend/zend_string.h
@@ -341,7 +341,7 @@ static zend_always_inline void zend_string_efree(zend_string *s)
efree(s);
}
-static zend_always_inline void zend_string_release(zend_string *s)
+ZEND_API inline void zend_string_release(zend_string *s)
{
if (!ZSTR_IS_INTERNED(s)) {
if (GC_DELREF(s) == 0) {
diff --git a/Zend/zend_types.h b/Zend/zend_types.h
index d1db25a8f2..85afd4316b 100644
--- a/Zend/zend_types.h
+++ b/Zend/zend_types.h
@@ -742,7 +742,7 @@ static zend_always_inline uint8_t zval_gc_type(uint32_t gc_type_info) {
return (gc_type_info & GC_TYPE_MASK);
}
-static zend_always_inline uint32_t zval_gc_flags(uint32_t gc_type_info) {
+ZEND_API inline uint32_t zval_gc_flags(uint32_t gc_type_info) {
return (gc_type_info >> GC_FLAGS_SHIFT) & (GC_FLAGS_MASK >> GC_FLAGS_SHIFT);
}
@@ -1339,7 +1339,7 @@ static zend_always_inline void zend_gc_try_delref(zend_refcounted_h *p) {
}
}
-static zend_always_inline uint32_t zend_gc_delref(zend_refcounted_h *p) {
+ZEND_API inline uint32_t zend_gc_delref(zend_refcounted_h *p) {
ZEND_ASSERT(p->refcount > 0);
ZEND_RC_MOD_CHECK(p);
return --(p->refcount);
However, it barely showed any savings (64 bytes). I didn't benchmark it, but since the size didn't change much I wouldn't expect there to be a significant change. I created a gcc report here that suggests being more conservative about inlining in unlikely branches. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=116191 |
Are we okay with me separating this into two PRs? One of them would just make a bunch of functions I want to have the APIs available to Rust extensions in PHP 8.4. The shrinking stuff is more of "well I'm making binaries bigger, and if there are complaints, it's probably going to be that, so let's be proactive in reducing it down." |
@morrisonlevi Would my patch here also work? It guarantees that the symbol with the given name is available, but it just also allows other compilation units to inline if they wish. It seems like you should be able to rely from your Rust source then that |
The patch works but I'd rather not touch it without benchmarks. As I mentioned before, there were cases where the compiler thought it would be beneficial to not inline but it looked to me to be on a not-cold path, so I didn't switch those to use the That doesn't mean it's not okay, we could perhaps make those choices depending on what comes up in the GCC discussion. For now I'm just trying to decouple such tinkerings so the extern APIs can be available in PHP 8.4. I was looking around and it seems there are other options, like writing this in a header: ZEND_API void zend_string_release(zend_string *s);
ZEND_API inline void zend_string_release(zend_string *s) { /* ... */ } Not quite sure how this stacks up in C and C++ yet. Preliminary investigation says that in C with clang it gets inlined but there is still a function emitted even without extern-inline. Not sure what other side effects it might have. Edit: it seems to emit it in every translation unit. Not what we want, though the linkers I tried didn't care. |
I don't think that's enough. You need
That's fair. If this is simply about making sure https://godbolt.org/z/GvqzshMfc Anyway, I don't object to your approach either. But once we introduce the |
About the name "outline": at least this term was familiar to me because I saw it in the compilers course I took as the opposite of inlining. AFAIK this is an existing term. Anyway, I don't care too much about the name, just pointing out that it's not that strange. "noinline" is fine by me. I'm not sure what approach is best honestly, I've seen the _noinline approach in other projects, but indeed once we add that we can't really go back. I don't object to either one though. |
I like the I don't object to either approach. |
BTW, here is an example where GCC wouldn't inline php-src/ext/standard/var_unserializer.re Line 1235 in 42497c1
This file has a few places it wouldn't inline. |
Can anyone check Windows that the new symbols exist with the correct visibility level? That's my only other concern other than |
@cmb69 👆 ? |
I think you can also add this line ( |
Oops, dumpbin is not in the PATH, and it might be a bit fiddly to get it there. So I've checked locally:
|
This reverts commit 027c451.
The main motivation here is to increase the amount of
ZEND_API
functions for working withzend_string *
. Rust extensions cannot easily access thesestatic zend_always_inline
functions. So this makes them available while also still forcing them to be inline in C.The secondary motivation is to shrink the size of the PHP binaries. It turns out that because many of these functions are forced inline, they actually bloat the binary. Consider code with a structure like this:
In cases like this, you do not actually want to inline
zend_string_release
. If you replacestatic zend_always_inline
withinline
and turn on-Werror=inline
, GCC will tell you this with an error message like:So, for some functions we want an inline version and a non-inline version. Sometimes we name a thing
x
and the inline version of itx_inline
. However, these are widely used APIs. So I left the inline version alone and added_outline
for the non-inline version. I'm open to a better naming scheme.Lastly,
-Werror=inline
has a lot of failures, as you might guess. This PR addresses a number of failures that fall into these classes that have nothing to do with strings, but cutting down the number of error messages helped me iterate over the patch:setjmp
, so it cannot be inline. Theinline
gets removed.inline
from this function.For these cases, the compiler was already not inlining the function. There shouldn't be any ill effect from this group of changes.
So what does the size look like after these changes? This is on Linux with GCC 14.1 in a docker container on an Apple M1 (aarch64) and configured with
CFLAGS="-O2 -g1 -fno-omit-frame-pointer"
.It saves 12,488 bytes, despite adding 25
ZEND_API
functions.Performance should not be affected, but there may be a slight improvement (some unexpected code paths become smaller, which can have cache effects). If there is a regression, it would mean that an
UNEXPECTED
(whether implicit or explicit) code path is actually taken quite often in the benchmark, and I'd like to know more information.