Bug 69789 - g++ -O2 is removing tests against a variable that can be changed
Summary: g++ -O2 is removing tests against a variable that can be changed
Status: UNCONFIRMED
Alias: None
Product: gcc
Classification: Unclassified
Component: target (show other bugs)
Version: 5.3.1
: P3 normal
Target Milestone: ---
Assignee: Not yet assigned to anyone
URL:
Keywords: wrong-code
Depends on:
Blocks:
 
Reported: 2016-02-12 21:03 UTC by Thomas Markwalder
Modified: 2016-08-23 15:14 UTC (History)
0 users

See Also:
Host:
Target: x86_64-redhat-linux
Build:
Known to work:
Known to fail:
Last reconfirmed:


Attachments
compressed compilation temp file (352.37 KB, application/x-gzip)
2016-02-12 21:06 UTC, Thomas Markwalder
Details

Note You need to log in before you can comment on or make changes to this bug.
Description Thomas Markwalder 2016-02-12 21:03:52 UTC
Our Kea project uses boost::asio. When compiled with gcc 5.3.1 and -O2, the optimizer is incorrectly removing tests against a variable which can be changed from one of the boost::asio functions.

1. gcc --version output:

Using built-in specs.
COLLECT_GCC=/usr/bin/gcc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/5.3.1/lto-wrapper
Target: x86_64-redhat-linux
Configured with: ../configure --enable-bootstrap --enable-languages=c,c++,objc,obj-c++,fortran,ada,go,lto --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-shared --enable-threads=posix --enable-checking=release --enable-multilib --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-linker-hash-style=gnu --enable-plugin --enable-initfini-array --disable-libgcj --with-isl --enable-libmpx --enable-gnu-indirect-function --with-tune=generic --with-arch_32=i686 --build=x86_64-redhat-linux
Thread model: posix
gcc version 5.3.1 20151207 (Red Hat 5.3.1-2) (GCC)

2. OS info:

Linux fedora23-64-1 4.3.4-300.fc23.x86_64 #1 SMP Mon Jan 25 13:39:23 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux

3. Compiler command line:

g++ -DHAVE_CONFIG_H -I. -I../../../..  -I../../../../src/lib -I../../../../src/lib  -DTEST_DATA_DIR=\"./testdata\" -I/opt/gtest/gtest-1.7.0 -I/opt/gtest/gtest-1.7.0/include -DOS_LINUX  -I../../../../ext/coroutine -DBOOST_ASIO_HEADER_ONLY -DBOOST_ASIO_DISABLE_THREADS=1 -DBOOST_ERROR_CODE_HEADER_ONLY -DBOOST_SYSTEM_NO_DEPRECATED -Wall -Wextra -Wnon-virtual-dtor -Wwrite-strings -Woverloaded-virtual -Wno-sign-compare -pthread -Werror -fPIC -Wno-missing-field-initializers -Wno-unused-parameter -g -O2 -save-temps -MT run_unittests-udp_socket_unittest.o -MD -MP -MF .deps/run_unittests-udp_socket_unittest.Tpo -c -o run_unittests-udp_socket_unittest.o `test -f 'udp_socket_unittest.cc' || echo './'`udp_socket_unittest.cc

4. I have attached the .ii file

5. BOOST Info:

Version 1.58

The affected function is in the file:

  /usr/include/boost/asio/detail/impl/socket_ops.ipp

and is excerpted below. The tests for ec against interrupted,
would_block, and try_again are optimized out, cauing the function
to always return true:


bool non_blocking_recvfrom(socket_type s,
    buf* bufs, size_t count, int flags,
    socket_addr_type* addr, std::size_t* addrlen,
    boost::system::error_code& ec, size_t& bytes_transferred)
{
  for (;;)
  {
    // Read some data.
    signed_size_type bytes = socket_ops::recvfrom(
        s, bufs, count, flags, addr, addrlen, ec);

    // Retry operation if interrupted by signal.
    if (ec == boost::asio::error::interrupted)
      continue;

    // Check if we need to run the operation again.
    if (ec == boost::asio::error::would_block
        || ec == boost::asio::error::try_again)
      return false;

    // Operation is complete.
    if (bytes >= 0)
    {
      ec = boost::system::error_code();
      bytes_transferred = bytes;
    }
    else
      bytes_transferred = 0;

    return true;
  }
}
Comment 1 Thomas Markwalder 2016-02-12 21:06:07 UTC
Created attachment 37676 [details]
compressed compilation temp file
Comment 2 Andrew Pinski 2016-02-13 07:34:22 UTC
This works in GCC 6 on aarch64 as far as I can tell.
Comment 3 Thomas Markwalder 2016-02-23 11:45:36 UTC
After further analysis we've discovered that the optimizer does not remove the tests, it alters the comparison of (ec == enumerated value).   If one replaces the "ec" with "ec.value()" in the expressions, such that you are explicitly comparing the interger error value the code works correctly when optimized:

{{{
     :

    // Retry operation if interrupted by signal.
    if (ec.value() == boost::asio::error::interrupted)
      continue;

    // Check if we need to run the operation again.
    if (ec.value() == boost::asio::error::would_block
        || ec == boost::asio::error::try_again)
      return false;

     :
}}}

Alternatively, adding an inline operator:

{{{
      inline friend bool operator==( const error_code & lhs,
                                     const int & rhs ) BOOST_SYSTEM_NOEXCEPT
      {
        return lhs.m_val == rhs;
      }
}}}

to error_code class also avoids the issue.
Comment 4 Tomek Mrugalski 2016-02-26 12:21:13 UTC
This issue also manifests itself on fully up-to-date Ubuntu 15.10, which has the following version g++ (Ubuntu 5.2.1-22ubuntu2) 5.2.1 20151010.

Here's also a bug report that confirms this issue on Fedora 23:

https://lists.isc.org/pipermail/kea-users/2016-February/000232.html, 
https://lists.isc.org/pipermail/kea-users/2016-February/000234.html

The reported version there is:

g++ (GCC) 5.3.1 20151207 (Red Hat 5.3.1-2)
Comment 5 Thomas Markwalder 2016-03-15 13:26:56 UTC
A bit more digging reveals that in the logic expression which fails:

{{{
    // Check if we need to run the operation again.
    if (ec == boost::asio::error::would_block
        || ec == boost::asio::error::try_again)
      return false;
}}}

The value for error_category:m_cat differs between the implicitly created instances for would_block and try_again.

If one splits the if into two separate if statements,  the values for m_cat are the same.  This looks a like the behavior described under "Static Variables in Inline Functions" described here:

http://processors.wiki.ti.com/index.php/C++_Inlining_Issues
Comment 6 Tomek Mrugalski 2016-08-19 08:16:35 UTC
This issue manifests itself also on g++ 5.4.0, the default compiler in Ubuntu 16.04. Exact version: (Ubuntu 5.4.0-6ubuntu1~16.04.2) 5.4.0 20160609.
Comment 7 Jonathan Wakely 2016-08-19 09:33:02 UTC
(In reply to Thomas Markwalder from comment #5)
> A bit more digging reveals that in the logic expression which fails:
> 
> {{{
>     // Check if we need to run the operation again.
>     if (ec == boost::asio::error::would_block
>         || ec == boost::asio::error::try_again)
>       return false;
> }}}
> 
> The value for error_category:m_cat differs between the implicitly created
> instances for would_block and try_again.

Do you mean that error_code(boost::asio::error::would_block) != error_code(boost::asio::error::try_again) ?

I don't see how that's possible, since in the optimized GIMPLE dump they both use the same constructor, which isn't inlined. The two enums have the same value, and the constructor will call the same asio::make_error_code() overload that sets the category to boost::system_category().

              boost::system::error_code::error_code<boost::asio::error::basic_errors> (&D.94931, 11, 0B);
              cleanup.88 = 1;
              D.165192 = boost::system::operator== (ec, &D.94931);
              if (D.165192 != 0) goto <D.165186>; else goto <D.165193>;
              <D.165193>:
              boost::system::error_code::error_code<boost::asio::error::basic_errors> (&D.94932, 11, 0B);
              cleanup.89 = 1;
              D.165197 = boost::system::operator== (ec, &D.94932);
              if (D.165197 != 0) goto <D.165186>; else goto <D.165187>;
              <D.165186>:
              iftmp.87 = 1;
              goto <D.165188>;
              <D.165187>:
              iftmp.87 = 0;
              <D.165188>:
              retval.86 = iftmp.87;


So what exactly are you seeing? What digging have you done? Stepped through with the debugger?

The most likely explanation seems to be that ec.category() != boost::system::system_category() and so the comparisons are always false.


> If one splits the if into two separate if statements,  the values for m_cat
> are the same.  This looks a like the behavior described under "Static
> Variables in Inline Functions" described here:
> 
> http://processors.wiki.ti.com/index.php/C++_Inlining_Issues

This function has nothing to do with static variables.
Comment 8 Thomas Markwalder 2016-08-23 15:14:16 UTC
(In reply to Jonathan Wakely from comment #7)
> (In reply to Thomas Markwalder from comment #5)
> > A bit more digging reveals that in the logic expression which fails:
> > 
> > {{{
> >     // Check if we need to run the operation again.
> >     if (ec == boost::asio::error::would_block
> >         || ec == boost::asio::error::try_again)
> >       return false;
> > }}}
> > 
> > The value for error_category:m_cat differs between the implicitly created
> > instances for would_block and try_again.
> 
> Do you mean that error_code(boost::asio::error::would_block) !=
> error_code(boost::asio::error::try_again) ?
> 
> I don't see how that's possible, since in the optimized GIMPLE dump they
> both use the same constructor, which isn't inlined. The two enums have the
> same value, and the constructor will call the same asio::make_error_code()
> overload that sets the category to boost::system_category().
> 
>              
> boost::system::error_code::error_code<boost::asio::error::basic_errors>
> (&D.94931, 11, 0B);
>               cleanup.88 = 1;
>               D.165192 = boost::system::operator== (ec, &D.94931);
>               if (D.165192 != 0) goto <D.165186>; else goto <D.165193>;
>               <D.165193>:
>              
> boost::system::error_code::error_code<boost::asio::error::basic_errors>
> (&D.94932, 11, 0B);
>               cleanup.89 = 1;
>               D.165197 = boost::system::operator== (ec, &D.94932);
>               if (D.165197 != 0) goto <D.165186>; else goto <D.165187>;
>               <D.165186>:
>               iftmp.87 = 1;
>               goto <D.165188>;
>               <D.165187>:
>               iftmp.87 = 0;
>               <D.165188>:
>               retval.86 = iftmp.87;
> 
> 
> So what exactly are you seeing? What digging have you done? Stepped through
> with the debugger?
> 
> The most likely explanation seems to be that ec.category() !=
> boost::system::system_category() and so the comparisons are always false.
> 
> 
> > If one splits the if into two separate if statements,  the values for m_cat
> > are the same.  This looks a like the behavior described under "Static
> > Variables in Inline Functions" described here:
> > 
> > http://processors.wiki.ti.com/index.php/C++_Inlining_Issues
> 
> This function has nothing to do with static variables.

Hello Jonathan:

It has been some months since I looked at this so let me try to recap. 
I did do a great deal of stepping through with the debugger as well as
instrumented the Boost code with couts in various places.  The basic issue
seems to be that when you build Boost such that BOOST_ERROR_CODE_HEADER_ONLY is 
defined, then the following function in <boost>/system/detail/error_code.ipp
is declared as inline:

  : 
# ifdef BOOST_ERROR_CODE_HEADER_ONLY
#   define BOOST_SYSTEM_LINKAGE inline
# else
#   define BOOST_SYSTEM_LINKAGE BOOST_SYSTEM_DECL
# endif

    BOOST_SYSTEM_LINKAGE const error_category & system_category() BOOST_SYSTEM_NOEXCEPT
    {
      static const system_error_category  system_category_const;
      return system_category_const;
    }
  :

Apparently some combination of boost includes along with our own, and building
with optimization, we end up with more than once instance of this function
each with its own value for the static variable, system_error_category.  Thus
the value returned by invoking system_category() depends upon which instance
is invoked. The result is the aforementioned if-clause:

    // Check if we need to run the operation again.
    if (ec == boost::asio::error::would_block
        || ec == boost::asio::error::try_again)
      return false;

not evaluating to true even though it should. I confirmed with the debugger
that  ec.m_cat does not reliably equal would_block.m_cat or try_again.m_cat.
If you split the clause into two separate statements it works reliably:

    // Check if we need to run the operation again.
    if (ec == boost::asio::error::would_block)
      return false;

    // Check if we need to run the operation again.
    if (ec == boost::asio::error::try_again)
      return false;

Not necessarily meaningful, but noteworthy, and of course, I can't use that
as a solution as that's within the boost code.

If one compiles without optimization or one builds with Boost system library 
rather than with BOOST_ERROR_CODE_HEADER_ONLY defined, the issue disappears.
I did try to replicate it with only boost code and could not do so.  I think
there is some corner case we are hitting in permits there to be more than
one static instance of system_error_category.