The @ operator costs you even when nothing goes wrong

The error suppression operator looks like a targeted silence — this one call, this one expected warning. It is neither targeted nor free. PHP implements it by saving the current error_reporting level, setting it to zero, evaluating the expression and restoring the level, and it does all of that on every call whether or not anything goes wrong.

// suppression covers everything the expression reaches, not just this line:
// a warning raised three functions down is silenced too
$data = @unserialize($blob);

// the failure is still a failure; you have only removed the report of it
$data = unserialize($blob);

if (false === $data && $blob !== serialize(false)) {
    $log->warning('cache entry did not unserialise', array('key' => $key));
}

The scope is the part that does real damage. @ applies to the whole call tree below the expression, so suppressing an expected warning from a wrapper also suppresses an unrelated one from a database call four frames down — and that one had something to tell you. A registered error handler still runs for every suppressed error, which is why the standard handler body begins by consulting error_reporting(); if it does not, every @ in every dependency ends up in your log anyway. And with Xdebug loaded the save-and-restore is considerably more expensive than it sounds. Where a function documents a failure return, checking it costs one if and leaves the failure visible.