diff mbox series

[v3] tools/nolibc: fix up size inflate regression

Message ID 8eaab5da2dcbba42e3f3efc2ae686a22c95f84f0.1691386601.git.falcon@tinylab.org (mailing list archive)
State New
Headers show
Series [v3] tools/nolibc: fix up size inflate regression | expand

Commit Message

Zhangjin Wu Aug. 7, 2023, 5:57 a.m. UTC
As reported and suggested by Willy, the inline __sysret() helper
introduces three types of conversions and increases the size:

(1) the "unsigned long" argument to __sysret() forces a sign extension
from all sys_* functions that used to return 'int'

(2) the comparison with the error range now has to be performed on a
'unsigned long' instead of an 'int'

(3) the return value from __sysret() is a 'long' (note, a signed long)
which then has to be turned back to an 'int' before being returned by the
caller to satisfy the caller's prototype.

To fix up this, firstly, let's use macro instead of inline function to
preserves the input type and avoids these useless conversions (1), (3).

Secondly, comparison to -MAX_ERRNO inflicts on all integer returns where
we could previously keep a simple sign comparison, let's use a new
is_signed_type() macro from include/linux/compiler.h to limit the
comparision to -MAX_ERRNO (2) only on demand and preserves a simple sign
comparision for most of the cases as before.

Thirdly, fix up the following warning by an explicit conversion and let
__sysret() be able to accept the (void *) type of argument and return
value with the same (void *) type:

    sysroot/powerpc/include/sys.h: In function 'sbrk':
    sysroot/powerpc/include/sys.h:104:16: warning: cast to pointer from integer of different size [-Wint-to-pointer-cast]
      104 |         return (void *)__sysret(-ENOMEM);

Fourthly, to further workaround the argument type with 'const', must use
__auto_type for a new enough gcc versions and use 'long' for the old gcc
versions as before.

Here reports the size testing result with nolibc-test:

before:

    // ppc64le
    $ size nolibc-test
       text	   data	    bss	    dec	    hex	filename
      27916	      8	     80	  28004	   6d64	nolibc-test

    // mips
    $ size nolibc-test
       text	   data	    bss	    dec	    hex	filename
      23276	     64	     64	  23404	   5b6c	nolibc-test

after:

    // ppc64le
    $ size nolibc-test
       text	   data	    bss	    dec	    hex	filename
      27736	      8	     80	  27824	   6cb0	nolibc-test

    // mips
    $ size nolibc-test
       text	   data	    bss	    dec	    hex	filename
      23036	     64	     64	  23164	   5a7c	nolibc-test

Suggested-by: Willy Tarreau <w@1wt.eu>
Link: https://lore.kernel.org/lkml/20230806095846.GB10627@1wt.eu/
Link: https://lore.kernel.org/lkml/20230806134348.GA19145@1wt.eu/
Signed-off-by: Zhangjin Wu <falcon@tinylab.org>
---

Hi, Willy

To increase readability, v3 further defines a
__GXX_HAS_AUTO_TYPE_WITH_CONST_SUPPORT macro for gcc >= 11.0
(ABI_VERSION >= 1016) who has __auto_type with 'const' support.

When this macro is defined, provides a __sysret version with
__auto_type, otherwise, use a fixed 'long' type as a fallback.

Tested for all of the nolibc supported architectures with Arnd's
13.2.0 toolchains. and also for x86_64 with gcc-4.8 and gcc-9, no
compile failures, no compile warnings, no running failures.

Changes from v2 --> v3:

* define a __GXX_HAS_AUTO_TYPE_WITH_CONST_SUPPORT for gcc >= 11.0 (ABI_VERSION >= 1016)
* split __sysret() to two versions by the macro instead of a mixed unified and unreadable version
* use shorter __ret instead of __sysret_arg

Changes from v1 --> v2:

* fix up argument with 'const' in the type
* support "void *" argument

v2: https://lore.kernel.org/lkml/95fe3e732f455fab653fe1427118d905e4d04257.1691339836.git.falcon@tinylab.org/
v1: https://lore.kernel.org/lkml/20230806131921.52453-1-falcon@tinylab.org/

---
 tools/include/nolibc/sys.h | 66 +++++++++++++++++++++++++++++++-------
 1 file changed, 55 insertions(+), 11 deletions(-)

Comments

David Laight Aug. 7, 2023, 8:40 a.m. UTC | #1
From: Zhangjin Wu
> Sent: 07 August 2023 06:58
...
> +/* __auto_type is used instead of __typeof__ to workaround the build error
> + * 'error: assignment of read-only variable' when the argument has 'const' in
> + * the type, but __auto_type is a new feature from newer gcc version and it
> + * only works with 'const' from gcc 11.0 (__GXX_ABI_VERSION = 1016)
> + * https://gcc.gnu.org/legacy-ml/gcc-patches/2013-11/msg01378.html
> + */

You can use typeof((x) + 0) to lose the 'const' flag.
The only downside is that char/short become int.

> +
> +#if __GXX_ABI_VERSION >= 1016
> +#define __GXX_HAS_AUTO_TYPE_WITH_CONST_SUPPORT
> +#endif
> +
> +#ifdef __GXX_HAS_AUTO_TYPE_WITH_CONST_SUPPORT
> +#define __sysret(arg)                                                    \
> +({                                                                       \
> +	__auto_type __ret = (arg);                                       \
> +	if (__is_signed_type(__typeof__(arg))) {                         \
> +		if (__ret < 0) {                                         \
> +			SET_ERRNO(-(long)__ret);                         \
> +			__ret = (__typeof__(arg))(-1L);                  \
> +		}                                                        \
> +	} else {                                                         \
> +		if ((unsigned long)__ret >= (unsigned long)-MAX_ERRNO) { \
> +			SET_ERRNO(-(long)__ret);                         \
> +			__ret = (__typeof__(arg))(-1L);                  \
> +		}                                                        \
> +	}                                                                \
> +	__ret;                                                           \
> +})
> +
> +#else  /* ! __GXX_HAS_AUTO_TYPE_WITH_CONST_SUPPORT */
> +#define __sysret(arg)                                                    \
> +({                                                                       \
> +	long __ret = (long)(arg);                                        \
> +	if (__is_signed_type(__typeof__(arg))) {                         \
> +		if (__ret < 0) {                                         \
> +			SET_ERRNO(-__ret);                               \
> +			__ret = -1L;                                     \
> +		}                                                        \
> +	} else {                                                         \
> +		if ((unsigned long)__ret >= (unsigned long)-MAX_ERRNO) { \
> +			SET_ERRNO(-__ret);                               \
> +			__ret = -1L;                                     \
> +		}                                                        \
> +	}                                                                \
> +	(__typeof__(arg))__ret;                                          \
> +})
> +#endif /* ! __GXX_HAS_AUTO_TYPE_WITH_CONST_SUPPORT */

with (retyped so it may be wrong):
#define is_constexpr(x) sizeof(*(0 ? (void *)((long)(x) * 0) : (int *)0)) == 1)
and (because even (void *)0 isn't completely constant):
#define is_pointer(x) (!is_constexpr((typeof(x))0))

You can probably do:
#define __sysret(arg) \
({ \
	typeof((arg) + 0) __ret = arg; \
	if (__built_choose_expr(is_pointer(arg), (unsigned long)-(MAX_ERRNO+1), __ret) \
			< (__built_choose_expr(is_pointer(arg), (unsigned long)__ret, 0)) { \
		SET_ERRNO(-__ret); \
		__reg = typeof(ret)-1L; \
	} \
	__ret; \
})

Apart from the annoyance of having to reverse the conditional
that only has one copy of the check.

Using two __builtin_choose_expr() saves you having to write two
comparisons that are valid for both pointer and integer.

	David

-
Registered Address Lakeside, Bramley Road, Mount Farm, Milton Keynes, MK1 1PT, UK
Registration No: 1397386 (Wales)
David Laight Aug. 7, 2023, 9:53 a.m. UTC | #2
From: David Laight
> Sent: 07 August 2023 09:40
....
> with (retyped so it may be wrong):
> #define is_constexpr(x) sizeof(*(0 ? (void *)((long)(x) * 0) : (int *)0)) == 1)

Bah, I know why that works and I still got is backwards :-(
Basically the compiler needs to find a type that is 'compatible'
with both the possible results.
Since '(void *)0' is a valid 'int *' value that gives 'int *'.
But '(void *)(anything_else)' requires the 'int *' be converted
to 'void *'.

Also the following seems to compile to sane code
for all types on 32bit and 64bit x86.

int errno;

#define MAXERRNO 0x4000
#define type int *

type sysret(type arg)
{
    if ((unsigned long)arg < 0ul - MAXERRNO)
        return arg;

    errno = -(long)arg;
    return (__typeof(arg))-1;        
}

You do get a comparison but no sign extensions.

	David

-
Registered Address Lakeside, Bramley Road, Mount Farm, Milton Keynes, MK1 1PT, UK
Registration No: 1397386 (Wales)
Zhangjin Wu Aug. 7, 2023, 2:12 p.m. UTC | #3
Hi, David

> From: Zhangjin Wu
> > Sent: 07 August 2023 06:58
> ...
> > +/* __auto_type is used instead of __typeof__ to workaround the build error
> > + * 'error: assignment of read-only variable' when the argument has 'const' in
> > + * the type, but __auto_type is a new feature from newer gcc version and it
> > + * only works with 'const' from gcc 11.0 (__GXX_ABI_VERSION = 1016)
> > + * https://gcc.gnu.org/legacy-ml/gcc-patches/2013-11/msg01378.html
> > + */
> 
> You can use typeof((x) + 0) to lose the 'const' flag.
> The only downside is that char/short become int.
>

Great, thanks!

let's use it, and at least kill the branch using fixed 'long' type.

    #if __GXX_ABI_VERSION >= 1016
    #define __typeofdecl(arg) __auto_type
    #else
    #define __typeofdecl(arg) __typeof__(arg)
    #endif

    #define __sysret(arg)                                                    \
    ({                                                                       \
            __typeofdecl((arg) + 0) __ret = (arg);                           \
            if (__is_signed_type(__typeof__(arg))) {                         \
                    if ((long)__ret < 0) {                                   \
                            SET_ERRNO(-(long)__ret);                         \
                            __ret = (__typeof__(arg))-1L;                    \
                    }                                                        \
            } else {                                                         \
                    if ((unsigned long)__ret >= (unsigned long)-MAX_ERRNO) { \
                            SET_ERRNO(-(long)__ret);                         \
                            __ret = (__typeof__(arg))-1L;                    \
                    }                                                        \
            }                                                                \
            __ret;                                                           \
    })

My simple test on nolibc-test shows David's typeof solution does give
the same size result like __auto_type.

what's your suggestion? simply give up the '__auto_type' stuff and use
the generic __typeof__ version?

Willy, could you please test David's typeof solution on the one which
have 3-4% size inflating? or any other big programs using nolibc.

> > +
> > +#if __GXX_ABI_VERSION >= 1016
> > +#define __GXX_HAS_AUTO_TYPE_WITH_CONST_SUPPORT
> > +#endif
> > +
> > +#ifdef __GXX_HAS_AUTO_TYPE_WITH_CONST_SUPPORT
> > +#define __sysret(arg)                                                    \
> > +({                                                                       \
> > +	__auto_type __ret = (arg);                                       \
> > +	if (__is_signed_type(__typeof__(arg))) {                         \
> > +		if (__ret < 0) {                                         \
> > +			SET_ERRNO(-(long)__ret);                         \
> > +			__ret = (__typeof__(arg))(-1L);                  \
> > +		}                                                        \
> > +	} else {                                                         \
> > +		if ((unsigned long)__ret >= (unsigned long)-MAX_ERRNO) { \
> > +			SET_ERRNO(-(long)__ret);                         \
> > +			__ret = (__typeof__(arg))(-1L);                  \
> > +		}                                                        \
> > +	}                                                                \
> > +	__ret;                                                           \
> > +})
> > +
> > +#else  /* ! __GXX_HAS_AUTO_TYPE_WITH_CONST_SUPPORT */
> > +#define __sysret(arg)                                                    \
> > +({                                                                       \
> > +	long __ret = (long)(arg);                                        \
> > +	if (__is_signed_type(__typeof__(arg))) {                         \
> > +		if (__ret < 0) {                                         \
> > +			SET_ERRNO(-__ret);                               \
> > +			__ret = -1L;                                     \
> > +		}                                                        \
> > +	} else {                                                         \
> > +		if ((unsigned long)__ret >= (unsigned long)-MAX_ERRNO) { \
> > +			SET_ERRNO(-__ret);                               \
> > +			__ret = -1L;                                     \
> > +		}                                                        \
> > +	}                                                                \
> > +	(__typeof__(arg))__ret;                                          \
> > +})
> > +#endif /* ! __GXX_HAS_AUTO_TYPE_WITH_CONST_SUPPORT */
> 
> with (retyped so it may be wrong):
> #define is_constexpr(x) sizeof(*(0 ? (void *)((long)(x) * 0) : (int *)0)) == 1)
> and (because even (void *)0 isn't completely constant):
> #define is_pointer(x) (!is_constexpr((typeof(x))0))
> 
> You can probably do:
> #define __sysret(arg) \
> ({ \
> 	typeof((arg) + 0) __ret = arg; \
> 	if (__built_choose_expr(is_pointer(arg), (unsigned long)-(MAX_ERRNO+1), __ret) \
> 			< (__built_choose_expr(is_pointer(arg), (unsigned long)__ret, 0)) { \
> 		SET_ERRNO(-__ret); \
> 		__reg = typeof(ret)-1L; \
> 	} \
> 	__ret; \
> })
> 
> Apart from the annoyance of having to reverse the conditional
> that only has one copy of the check.
> 
> Using two __builtin_choose_expr() saves you having to write two
> comparisons that are valid for both pointer and integer.
>

It works perfectly.

    /*
     * This returns a constant expression while determining if an argument is
     * a constant expression, most importantly without evaluating the argument.
     * Glory to Martin Uecker <Martin.Uecker@med.uni-goettingen.de>
     * (from include/linux/const.h)
     */

    #define __is_constexpr(x) \
            (sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))

    #define __is_pointer(x) (!__is_constexpr((__typeof__(x))0))

    #define __sysret(arg)                                                                      \
    ({                                                                                         \
            __typeofdecl((arg) + 0) __ret = (arg);                                             \
            if (__builtin_choose_expr(__is_pointer(arg), (unsigned long)-(MAX_ERRNO + 1), ((long)__ret)) \
                    < __builtin_choose_expr(__is_pointer(arg), (unsigned long)__ret, 0)) {      \
                    SET_ERRNO(-(long)__ret);                                                   \
                    __ret = (__typeof__(arg))-1L;                                              \
            }                                                                                  \
            __ret;                                                                             \
    })

I have tried the 'is_constexpr()' macro but failed and didn't look into
it, your explanation here [1] is very clear:

    You'll find that (void *)0 isn't 'constant enough' for
    is_constexpr() - so is_constexpr((type)0) can be used
    to detect pointer types.

Best regards,
Zhangjin
---
[1]: https://lore.kernel.org/lkml/a1732bbffd1542d3b9dd34c92f45076c@AcuMS.aculab.com/

> 	David
> 
> -
> Registered Address Lakeside, Bramley Road, Mount Farm, Milton Keynes, MK1 1PT, UK
> Registration No: 1397386 (Wales)
diff mbox series

Patch

diff --git a/tools/include/nolibc/sys.h b/tools/include/nolibc/sys.h
index 56f63eb48a1b..b137f7771db9 100644
--- a/tools/include/nolibc/sys.h
+++ b/tools/include/nolibc/sys.h
@@ -35,15 +35,59 @@ 
  * (src/internal/syscall_ret.c) and glibc (sysdeps/unix/sysv/linux/sysdep.h)
  */
 
-static __inline__ __attribute__((unused, always_inline))
-long __sysret(unsigned long ret)
-{
-	if (ret >= (unsigned long)-MAX_ERRNO) {
-		SET_ERRNO(-(long)ret);
-		return -1;
-	}
-	return ret;
-}
+/*
+ * Whether 'type' is a signed type or an unsigned type. Supports scalar types,
+ * bool and also pointer types. (from include/linux/compiler.h)
+ */
+#define __is_signed_type(type) (((type)(-1)) < (type)1)
+
+/* __auto_type is used instead of __typeof__ to workaround the build error
+ * 'error: assignment of read-only variable' when the argument has 'const' in
+ * the type, but __auto_type is a new feature from newer gcc version and it
+ * only works with 'const' from gcc 11.0 (__GXX_ABI_VERSION = 1016)
+ * https://gcc.gnu.org/legacy-ml/gcc-patches/2013-11/msg01378.html
+ */
+
+#if __GXX_ABI_VERSION >= 1016
+#define __GXX_HAS_AUTO_TYPE_WITH_CONST_SUPPORT
+#endif
+
+#ifdef __GXX_HAS_AUTO_TYPE_WITH_CONST_SUPPORT
+#define __sysret(arg)                                                    \
+({                                                                       \
+	__auto_type __ret = (arg);                                       \
+	if (__is_signed_type(__typeof__(arg))) {                         \
+		if (__ret < 0) {                                         \
+			SET_ERRNO(-(long)__ret);                         \
+			__ret = (__typeof__(arg))(-1L);                  \
+		}                                                        \
+	} else {                                                         \
+		if ((unsigned long)__ret >= (unsigned long)-MAX_ERRNO) { \
+			SET_ERRNO(-(long)__ret);                         \
+			__ret = (__typeof__(arg))(-1L);                  \
+		}                                                        \
+	}                                                                \
+	__ret;                                                           \
+})
+
+#else  /* ! __GXX_HAS_AUTO_TYPE_WITH_CONST_SUPPORT */
+#define __sysret(arg)                                                    \
+({                                                                       \
+	long __ret = (long)(arg);                                        \
+	if (__is_signed_type(__typeof__(arg))) {                         \
+		if (__ret < 0) {                                         \
+			SET_ERRNO(-__ret);                               \
+			__ret = -1L;                                     \
+		}                                                        \
+	} else {                                                         \
+		if ((unsigned long)__ret >= (unsigned long)-MAX_ERRNO) { \
+			SET_ERRNO(-__ret);                               \
+			__ret = -1L;                                     \
+		}                                                        \
+	}                                                                \
+	(__typeof__(arg))__ret;                                          \
+})
+#endif /* ! __GXX_HAS_AUTO_TYPE_WITH_CONST_SUPPORT */
 
 /* Functions in this file only describe syscalls. They're declared static so
  * that the compiler usually decides to inline them while still being allowed
@@ -94,7 +138,7 @@  void *sbrk(intptr_t inc)
 	if (ret && sys_brk(ret + inc) == ret + inc)
 		return ret + inc;
 
-	return (void *)__sysret(-ENOMEM);
+	return __sysret((void *)-ENOMEM);
 }
 
 
@@ -682,7 +726,7 @@  void *sys_mmap(void *addr, size_t length, int prot, int flags, int fd,
 static __attribute__((unused))
 void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)
 {
-	return (void *)__sysret((unsigned long)sys_mmap(addr, length, prot, flags, fd, offset));
+	return __sysret(sys_mmap(addr, length, prot, flags, fd, offset));
 }
 
 static __attribute__((unused))